Merge pull request #79 from cloudbuilders/snapshots
Support for instance snapshots
This commit is contained in:
commit
c99a11bc39
@ -383,6 +383,17 @@ def image_list_detailed(request):
|
|||||||
return [Image(i) for i in glance_api(request).get_images_detailed()]
|
return [Image(i) for i in glance_api(request).get_images_detailed()]
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_list_detailed(request):
|
||||||
|
filters = {}
|
||||||
|
filters['property-image_type'] = 'snapshot'
|
||||||
|
filters['is_public'] = 'none'
|
||||||
|
return [Image(i) for i in glance_api(request)
|
||||||
|
.get_images_detailed(filters=filters)]
|
||||||
|
|
||||||
|
def snapshot_create(request, instance_id, name):
|
||||||
|
return extras_api(request).snapshots.create(instance_id, name)
|
||||||
|
|
||||||
|
|
||||||
def image_update(request, image_id, image_meta=None):
|
def image_update(request, image_id, image_meta=None):
|
||||||
image_meta = image_meta and image_meta or {}
|
image_meta = image_meta and image_meta or {}
|
||||||
return Image(glance_api(request).update_image(image_id,
|
return Image(glance_api(request).update_image(image_id,
|
||||||
|
@ -23,6 +23,7 @@ from django.conf.urls.defaults import *
|
|||||||
INSTANCES = r'^(?P<tenant_id>[^/]+)/instances/(?P<instance_id>[^/]+)/%s$'
|
INSTANCES = r'^(?P<tenant_id>[^/]+)/instances/(?P<instance_id>[^/]+)/%s$'
|
||||||
IMAGES = r'^(?P<tenant_id>[^/]+)/images/(?P<image_id>[^/]+)/%s$'
|
IMAGES = r'^(?P<tenant_id>[^/]+)/images/(?P<image_id>[^/]+)/%s$'
|
||||||
KEYPAIRS = r'^(?P<tenant_id>[^/]+)/keypairs/%s$'
|
KEYPAIRS = r'^(?P<tenant_id>[^/]+)/keypairs/%s$'
|
||||||
|
SNAPSHOTS = r'^(?P<tenant_id>[^/]+)/snapshots/(?P<instance_id>[^/]+)/%s$'
|
||||||
CONTAINERS = r'^(?P<tenant_id>[^/]+)/containers/%s$'
|
CONTAINERS = r'^(?P<tenant_id>[^/]+)/containers/%s$'
|
||||||
OBJECTS = r'^(?P<tenant_id>[^/]+)/containers/(?P<container_name>[^/]+)/%s$'
|
OBJECTS = r'^(?P<tenant_id>[^/]+)/containers/(?P<container_name>[^/]+)/%s$'
|
||||||
|
|
||||||
@ -45,6 +46,11 @@ urlpatterns += patterns('django_openstack.dash.views.keypairs',
|
|||||||
url(KEYPAIRS % 'create', 'create', name='dash_keypairs_create'),
|
url(KEYPAIRS % 'create', 'create', name='dash_keypairs_create'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
urlpatterns += patterns('django_openstack.dash.views.snapshots',
|
||||||
|
url(r'^(?P<tenant_id>[^/]+)/snapshots/$', 'index', name='dash_snapshots'),
|
||||||
|
url(SNAPSHOTS % 'create', 'create', name='dash_snapshots_create'),
|
||||||
|
)
|
||||||
|
|
||||||
# Swift containers and objects.
|
# Swift containers and objects.
|
||||||
urlpatterns += patterns('django_openstack.dash.views.containers',
|
urlpatterns += patterns('django_openstack.dash.views.containers',
|
||||||
url(CONTAINERS % '', 'index', name='dash_containers'),
|
url(CONTAINERS % '', 'index', name='dash_containers'),
|
||||||
|
114
django-openstack/django_openstack/dash/views/snapshots.py
Normal file
114
django-openstack/django_openstack/dash/views/snapshots.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Copyright 2011 Fourth Paradigm Development, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Views for managing Nova instance snapshots.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django import http
|
||||||
|
from django import template
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect, render_to_response
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
from django import shortcuts
|
||||||
|
|
||||||
|
from django_openstack import api
|
||||||
|
from django_openstack import forms
|
||||||
|
from openstackx.api import exceptions as api_exceptions
|
||||||
|
from glance.common import exception as glance_exception
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger('django_openstack.dash.views.snapshots')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateSnapshot(forms.SelfHandlingForm):
|
||||||
|
tenant_id = forms.CharField(widget=forms.HiddenInput())
|
||||||
|
instance_id = forms.CharField(widget=forms.TextInput(attrs={'readonly':'readonly'}))
|
||||||
|
name = forms.CharField(max_length="20", label="Snapshot Name")
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
LOG.info('Creating snapshot "%s"' % data['name'])
|
||||||
|
snapshot = api.snapshot_create(request, data['instance_id'], data['name'])
|
||||||
|
instance = api.server_get(request, data['instance_id'])
|
||||||
|
|
||||||
|
messages.info(request, 'Snapshot "%s" created for instance "%s"' %\
|
||||||
|
(data['name'], instance.name))
|
||||||
|
return shortcuts.redirect('dash_snapshots', data['tenant_id'])
|
||||||
|
except api_exceptions.ApiException, e:
|
||||||
|
msg = 'Error Creating Snapshot: %s' % e.message
|
||||||
|
LOG.error(msg, exc_info=True)
|
||||||
|
messages.error(request, msg)
|
||||||
|
return shortcuts.redirect(request.build_absolute_uri())
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def index(request, tenant_id):
|
||||||
|
images = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
images = api.snapshot_list_detailed(request)
|
||||||
|
except glance_exception.ClientConnectionError, e:
|
||||||
|
msg = 'Error connecting to glance: %s' % str(e)
|
||||||
|
LOG.error(msg, exc_info=True)
|
||||||
|
messages.error(request, msg)
|
||||||
|
except glance_exception.Error, e:
|
||||||
|
msg = 'Error retrieving image list: %s' % str(e)
|
||||||
|
LOG.error(msg, exc_info=True)
|
||||||
|
messages.error(request, msg)
|
||||||
|
|
||||||
|
return render_to_response('dash_snapshots.html', {
|
||||||
|
'images': images,
|
||||||
|
}, context_instance=template.RequestContext(request))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def create(request, tenant_id, instance_id):
|
||||||
|
form, handled = CreateSnapshot.maybe_handle(request,
|
||||||
|
initial={'tenant_id': tenant_id,
|
||||||
|
'instance_id': instance_id})
|
||||||
|
if handled:
|
||||||
|
return handled
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance = api.server_get(request, instance_id)
|
||||||
|
except api_exceptions.ApiException, e:
|
||||||
|
msg = "Unable to retreive instance: %s" % str(e)
|
||||||
|
LOG.error(msg)
|
||||||
|
messages.error(request, msg)
|
||||||
|
return shortcuts.redirect('dash_instances', tenant_id)
|
||||||
|
|
||||||
|
valid_states = ['ACTIVE']
|
||||||
|
if instance.status not in valid_states:
|
||||||
|
messages.error(request, "To snapshot, instance state must be\
|
||||||
|
one of the following: %s" %
|
||||||
|
', '.join(valid_states))
|
||||||
|
return shortcuts.redirect('dash_instances', tenant_id)
|
||||||
|
|
||||||
|
return shortcuts.render_to_response('dash_snapshots_create.html', {
|
||||||
|
'instance': instance,
|
||||||
|
'create_form': form,
|
||||||
|
}, context_instance=template.RequestContext(request))
|
@ -0,0 +1,189 @@
|
|||||||
|
from django import http
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django_openstack import api
|
||||||
|
from django_openstack.tests.view_tests import base
|
||||||
|
from glance.common import exception as glance_exception
|
||||||
|
from openstackx.api import exceptions as api_exceptions
|
||||||
|
from mox import IgnoreArg, IsA
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotsViewTests(base.BaseViewTests):
|
||||||
|
def setUp(self):
|
||||||
|
super(SnapshotsViewTests, self).setUp()
|
||||||
|
image_dict = {'name': 'snapshot',
|
||||||
|
'container_format': 'novaImage'}
|
||||||
|
self.images = [image_dict]
|
||||||
|
|
||||||
|
server = self.mox.CreateMock(api.Server)
|
||||||
|
server.id = 1
|
||||||
|
server.status = 'ACTIVE'
|
||||||
|
server.name = 'sgoody'
|
||||||
|
self.good_server = server
|
||||||
|
|
||||||
|
server = self.mox.CreateMock(api.Server)
|
||||||
|
server.id = 2
|
||||||
|
server.status = 'BUILD'
|
||||||
|
server.name = 'baddy'
|
||||||
|
self.bad_server = server
|
||||||
|
|
||||||
|
def test_index(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||||
|
api.snapshot_list_detailed(IsA(http.HttpRequest)).\
|
||||||
|
AndReturn(self.images)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(res, 'dash_snapshots.html')
|
||||||
|
|
||||||
|
self.assertIn('images', res.context)
|
||||||
|
images = res.context['images']
|
||||||
|
self.assertEqual(len(images), 1)
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_index_client_conn_error(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||||
|
exception = glance_exception.ClientConnectionError('clientConnError')
|
||||||
|
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(messages, 'error')
|
||||||
|
messages.error(IsA(http.HttpRequest), IsA(str))
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(res, 'dash_snapshots.html')
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_index_glance_error(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'snapshot_list_detailed')
|
||||||
|
exception = glance_exception.Error('glanceError')
|
||||||
|
api.snapshot_list_detailed(IsA(http.HttpRequest)).AndRaise(exception)
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(messages, 'error')
|
||||||
|
messages.error(IsA(http.HttpRequest), IsA(str))
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(res, 'dash_snapshots.html')
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_snapshot_get(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'server_get')
|
||||||
|
api.server_get(IsA(http.HttpRequest),
|
||||||
|
str(self.good_server.id)).AndReturn(self.good_server)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.good_server.id]))
|
||||||
|
|
||||||
|
self.assertTemplateUsed(res, 'dash_snapshots_create.html')
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_snapshot_get_with_invalid_status(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'server_get')
|
||||||
|
api.server_get(IsA(http.HttpRequest),
|
||||||
|
str(self.bad_server.id)).AndReturn(self.bad_server)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.bad_server.id]))
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, reverse('dash_instances',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_get_server_exception(self):
|
||||||
|
self.mox.StubOutWithMock(api, 'server_get')
|
||||||
|
exception = api_exceptions.ApiException('apiException')
|
||||||
|
api.server_get(IsA(http.HttpRequest),
|
||||||
|
str(self.good_server.id)).AndRaise(exception)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.good_server.id]))
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, reverse('dash_instances',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_snapshot_post(self):
|
||||||
|
SNAPSHOT_NAME = 'snappy'
|
||||||
|
|
||||||
|
new_snapshot = self.mox.CreateMock(api.Image)
|
||||||
|
new_snapshot.name = SNAPSHOT_NAME
|
||||||
|
|
||||||
|
formData = {'method': 'CreateSnapshot',
|
||||||
|
'tenant_id': self.TEST_TENANT,
|
||||||
|
'instance_id': self.good_server.id,
|
||||||
|
'name': SNAPSHOT_NAME}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(api, 'server_get')
|
||||||
|
api.server_get(IsA(http.HttpRequest),
|
||||||
|
str(self.good_server.id)).AndReturn(self.good_server)
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(api, 'snapshot_create')
|
||||||
|
api.snapshot_create(IsA(http.HttpRequest),
|
||||||
|
str(self.good_server.id), SNAPSHOT_NAME).\
|
||||||
|
AndReturn(new_snapshot)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.post(reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.good_server.id]),
|
||||||
|
formData)
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, reverse('dash_snapshots',
|
||||||
|
args=[self.TEST_TENANT]))
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_snapshot_post_exception(self):
|
||||||
|
SNAPSHOT_NAME = 'snappy'
|
||||||
|
|
||||||
|
new_snapshot = self.mox.CreateMock(api.Image)
|
||||||
|
new_snapshot.name = SNAPSHOT_NAME
|
||||||
|
|
||||||
|
formData = {'method': 'CreateSnapshot',
|
||||||
|
'tenant_id': self.TEST_TENANT,
|
||||||
|
'instance_id': self.good_server.id,
|
||||||
|
'name': SNAPSHOT_NAME}
|
||||||
|
|
||||||
|
self.mox.StubOutWithMock(api, 'snapshot_create')
|
||||||
|
exception = api_exceptions.ApiException('apiException',
|
||||||
|
message='apiException')
|
||||||
|
api.snapshot_create(IsA(http.HttpRequest),
|
||||||
|
str(self.good_server.id), SNAPSHOT_NAME).\
|
||||||
|
AndRaise(exception)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.post(reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.good_server.id]),
|
||||||
|
formData)
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, reverse('dash_snapshots_create',
|
||||||
|
args=[self.TEST_TENANT,
|
||||||
|
self.good_server.id]))
|
||||||
|
|
||||||
|
self.mox.VerifyAll()
|
@ -4,6 +4,7 @@
|
|||||||
<li><a {% if current_sidebar == "overview" %} class="active" {% endif %} href="{% url dash_overview %}">Overview</a></li>
|
<li><a {% if current_sidebar == "overview" %} class="active" {% endif %} href="{% url dash_overview %}">Overview</a></li>
|
||||||
<li><a {% if current_sidebar == "instances" %} class="active" {% endif %} href="{% url dash_instances request.user.tenant %}">Instances</a></li>
|
<li><a {% if current_sidebar == "instances" %} class="active" {% endif %} href="{% url dash_instances request.user.tenant %}">Instances</a></li>
|
||||||
<li><a {% if current_sidebar == "images" %} class="active" {% endif %} href="{% url dash_images request.user.tenant %}">Images</a></li>
|
<li><a {% if current_sidebar == "images" %} class="active" {% endif %} href="{% url dash_images request.user.tenant %}">Images</a></li>
|
||||||
|
<li><a {% if current_sidebar == "snapshots" %} class="active" {% endif %} href="{% url dash_snapshots request.user.tenant %}">Snapshots</a></li>
|
||||||
<li><a {% if current_sidebar == "keypairs" %} class="active" {% endif %} href="{% url dash_keypairs request.user.tenant %}">Keypairs</a></li>
|
<li><a {% if current_sidebar == "keypairs" %} class="active" {% endif %} href="{% url dash_keypairs request.user.tenant %}">Keypairs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% if swift_configured %}
|
{% if swift_configured %}
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
<li><a target="_blank" href="{% url dash_instances_console request.user.tenant instance.id %}">Log</a></li>
|
<li><a target="_blank" href="{% url dash_instances_console request.user.tenant instance.id %}">Log</a></li>
|
||||||
<li><a target="_blank" href="{% url dash_instances_vnc request.user.tenant instance.id %}">VNC Console</a></li>
|
<li><a target="_blank" href="{% url dash_instances_vnc request.user.tenant instance.id %}">VNC Console</a></li>
|
||||||
<li><a href="{% url dash_instances_update request.user.tenant instance.id %}">Edit</a></li>
|
<li><a href="{% url dash_instances_update request.user.tenant instance.id %}">Edit</a></li>
|
||||||
|
<li><a href="{% url dash_snapshots_create request.user.tenant instance.id %}">Snapshot</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
13
openstack-dashboard/dashboard/templates/_snapshot_form.html
Normal file
13
openstack-dashboard/dashboard/templates/_snapshot_form.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<form id="snapshot_form" method="post">
|
||||||
|
<fieldset>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{{ field.label_tag }}
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" value="Create Snapshot" class="large-rounded" />
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
25
openstack-dashboard/dashboard/templates/dash_snapshots.html
Normal file
25
openstack-dashboard/dashboard/templates/dash_snapshots.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends 'dash_base.html' %}
|
||||||
|
|
||||||
|
{% block sidebar %}
|
||||||
|
{% with current_sidebar="snapshots" %}
|
||||||
|
{{block.super}}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% url dash_snapshots request.user.tenant as refresh_link %}
|
||||||
|
{# to make searchable false, just remove it from the include statement #}
|
||||||
|
{% include "_page_header.html" with title="Snapshots" refresh_link=refresh_link searchable="true" %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block dash_main %}
|
||||||
|
{% if images %}
|
||||||
|
{% include '_image_list.html' %}
|
||||||
|
{% else %}
|
||||||
|
<div class="message_box info">
|
||||||
|
<h2>Info</h2>
|
||||||
|
<p>There are currently no snapshots. You can create snapshots from running instances. <a href='{% url dash_instances request.user.tenant %}'>View Running Instances >></a></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,37 @@
|
|||||||
|
{% extends 'dash_base.html' %}
|
||||||
|
|
||||||
|
{% block sidebar %}
|
||||||
|
{% with current_sidebar="snapshots" %}
|
||||||
|
{{block.super}}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block headerjs %}
|
||||||
|
<script type="text/javascript" charset="utf-8">
|
||||||
|
$(function(){
|
||||||
|
$("#id_name").focus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "_page_header.html" with title="Create a Snapshot" %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block dash_main %}
|
||||||
|
<div class="dash_block">
|
||||||
|
<div class="left">
|
||||||
|
<h3>Choose a name for your snapshot.</h3>
|
||||||
|
{% include '_snapshot_form.html' with form=create_form %}
|
||||||
|
<h3><a href="{% url dash_snapshots request.user.tenant %}"><< Return to snapshots list</a></h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<h3>Description:</h3>
|
||||||
|
<p>Snapshots preserve the disk state of a running instance.</p>
|
||||||
|
</div>
|
||||||
|
<div class="clear"> </div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user