Parcourir la source

实现快照管理功能中

James Iter il y a 8 ans
Parent
commit
007d65b400

+ 194 - 0
api/snapshot.py

@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from flask import Blueprint
+from flask import request
+import json
+import jimit as ji
+
+from api.base import Base
+from models import Guest
+from models import Snapshot, SnapshotDiskMapping
+from models import Utils
+from models import Rules
+
+
+__author__ = 'James Iter'
+__date__ = '2018/4/10'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_snapshot',
+    __name__,
+    url_prefix='/api/snapshot'
+)
+
+blueprints = Blueprint(
+    'api_snapshots',
+    __name__,
+    url_prefix='/api/snapshots'
+)
+
+
+snapshot_base = Base(the_class=Snapshot, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.GUEST_UUID.value
+    ]
+
+    if 'label' in request.json:
+        args_rules.append(
+            Rules.LABEL.value,
+        )
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        snapshot = Snapshot()
+        guest = Guest()
+        guest.uuid = request.json.get('guest_uuid')
+        guest.get_by('uuid')
+
+        snapshot.label = request.json.get('label', '')
+        snapshot.status = guest.status
+        snapshot.guest_uuid = guest.uuid
+        snapshot.snapshot_id = '_'.join(['tmp', ji.Common.generate_random_code(length=8)])
+        snapshot.parent_id = '-'
+        snapshot.progress = 0
+
+        snapshot.create()
+        snapshot.get_by('snapshot_id')
+
+        message = {
+            '_object': 'snapshot',
+            'action': 'create',
+            'uuid': guest.uuid,
+            'node_id': guest.node_id,
+            'passback_parameters': {'id': snapshot.id}
+        }
+
+        Utils.emit_instruction(message=json.dumps(message, ensure_ascii=False))
+
+        ret['data'] = snapshot.__dict__
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(snapshot_id):
+
+    snapshot = Snapshot()
+
+    args_rules = [
+        Rules.SNAPSHOT_ID.value
+    ]
+
+    if 'label' in request.json:
+        args_rules.append(
+            Rules.LABEL.value,
+        )
+
+    if args_rules.__len__() < 2:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    request.json['snapshot_id'] = snapshot_id
+
+    try:
+        ji.Check.previewing(args_rules, request.json)
+        snapshot.snapshot_id = request.json.get('snapshot_id')
+
+        snapshot.get_by('snapshot_id')
+        snapshot.label = request.json.get('label', snapshot.label)
+
+        snapshot.update()
+        snapshot.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = snapshot.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(snapshots_id):
+    return snapshot_base.get(ids=snapshots_id, ids_rule=Rules.SNAPSHOTS_ID.value, by_field='snapshot_id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return snapshot_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return snapshot_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(snapshots_id):
+
+    args_rules = [
+        Rules.SNAPSHOTS_ID.value
+    ]
+
+    try:
+        ji.Check.previewing(args_rules, {'snapshots_id': snapshots_id})
+
+        snapshot = Snapshot()
+        guest = Guest()
+
+        # 检测所指定的 快照 都存在
+        for snapshot_id in snapshots_id.split(','):
+            snapshot.snapshot_id = snapshot_id
+            snapshot.get_by('snapshot_id')
+
+            guest.uuid = snapshot.guest_uuid
+            guest.get_by('uuid')
+
+        # 执行删除操作
+        for snapshot_id in snapshots_id.split(','):
+            snapshot.snapshot_id = snapshot_id
+            snapshot.get_by('snapshot_id')
+
+            guest.uuid = snapshot.guest_uuid
+            guest.get_by('uuid')
+
+            message = {
+                '_object': 'snapshot',
+                'action': 'delete',
+                'uuid': snapshot.guest_uuid,
+                'snapshot_id': snapshot.snapshot_id,
+                'node_id': guest.node_id,
+                'passback_parameters': {'id': snapshot.id}
+            }
+
+            Utils.emit_instruction(message=json.dumps(message))
+
+            # 删除创建失败的 快照
+            if snapshot.progress == 255:
+                SnapshotDiskMapping.delete_by_filter(filter_str=':'.join(['snapshot_id', 'eq', snapshot.snapshot_id]))
+                snapshot.delete()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 9 - 0
api_route_table.py

@@ -7,6 +7,7 @@ from api import config
 from api import user
 from api import guest
 from api import disk
+from api import snapshot
 from api import os_template_image
 from api import os_template_profile
 from api import os_template_initialize_operate_set
@@ -141,6 +142,14 @@ add_rule_api(ssh_key.blueprint, '/_unbound/<ssh_key_id>', api_func='ssh_key.r_un
 add_rule_api(ssh_key.blueprint, '/_bind/<ssh_key_id>/<uuids>', api_func='ssh_key.r_bind', methods=['PUT'])
 add_rule_api(ssh_key.blueprint, '/_unbind/<ssh_key_id>/<uuids>', api_func='ssh_key.r_unbind', methods=['PUT'])
 
+# 快照操作
+add_rule_api(snapshot.blueprint, '', api_func='snapshot.r_create', methods=['POST'])
+add_rule_api(snapshot.blueprints, '/<snapshots_id>', api_func='snapshot.r_delete', methods=['DELETE'])
+add_rule_api(snapshot.blueprints, '/<snapshot_id>', api_func='snapshot.r_update', methods=['PATCH'])
+add_rule_api(snapshot.blueprints, '/<snapshots_id>', api_func='snapshot.r_get', methods=['GET'])
+add_rule_api(snapshot.blueprints, '', api_func='snapshot.r_get_by_filter', methods=['GET'])
+add_rule_api(snapshot.blueprints, '/_search', api_func='snapshot.r_content_search', methods=['GET'])
+
 # 日志查询
 # Guest 性能查询
 add_rule_api(guest_performance.blueprint, '/cpu_memory',

+ 1 - 0
docs/todo.md

@@ -79,5 +79,6 @@
 - [x] 取消单独的初始化密码操作,合并入具体的操作系统初始化操作中
 - [ ] 迁移中的虚拟机,不允许做任何操作
 - [ ] 通过 QemuGuestAgent 实现 Guest 的内存使用率监控
+- [ ] 修复 Guest 对模板对象的依赖。bug 表现为,当依赖的模板项被删除后,虚拟机实例列表会出现 500 错误
 
 

+ 8 - 0
main.py

@@ -45,6 +45,8 @@ from api.host import blueprint as host_blueprint
 from api.host import blueprints as host_blueprints
 from api.ssh_key import blueprint as ssh_key_blueprint
 from api.ssh_key import blueprints as ssh_key_blueprints
+from api.snapshot import blueprint as snapshot_blueprint
+from api.snapshot import blueprints as snapshot_blueprints
 from api.guest_performance import blueprint as performance_blueprint
 from api.guest_performance import blueprints as performance_blueprints
 from api.host_performance import blueprint as host_performance_blueprint
@@ -64,6 +66,8 @@ from views.os_template_image import blueprint as view_os_template_image_blueprin
 from views.os_template_image import blueprints as view_os_template_image_blueprints
 from views.ssh_key import blueprint as view_ssh_key_blueprint
 from views.ssh_key import blueprints as view_ssh_key_blueprints
+from views.snapshot import blueprint as view_snapshot_blueprint
+from views.snapshot import blueprints as view_snapshot_blueprints
 
 from views.host import blueprint as view_host_blueprint
 from views.host import blueprints as view_host_blueprints
@@ -235,6 +239,8 @@ try:
     app.register_blueprint(host_blueprints)
     app.register_blueprint(ssh_key_blueprint)
     app.register_blueprint(ssh_key_blueprints)
+    app.register_blueprint(snapshot_blueprint)
+    app.register_blueprint(snapshot_blueprints)
     app.register_blueprint(performance_blueprint)
     app.register_blueprint(performance_blueprints)
     app.register_blueprint(host_performance_blueprint)
@@ -253,6 +259,8 @@ try:
     app.register_blueprint(view_os_template_image_blueprints)
     app.register_blueprint(view_ssh_key_blueprint)
     app.register_blueprint(view_ssh_key_blueprints)
+    app.register_blueprint(view_snapshot_blueprint)
+    app.register_blueprint(view_snapshot_blueprints)
 
     app.register_blueprint(view_host_blueprint)
     app.register_blueprint(view_host_blueprints)

+ 31 - 0
misc/init.sql

@@ -381,6 +381,37 @@ ALTER TABLE ssh_key_guest_mapping ADD UNIQUE INDEX (ssh_key_id, guest_uuid);
 ALTER TABLE ssh_key_guest_mapping ADD INDEX (guest_uuid);
 
 
+CREATE TABLE IF NOT EXISTS snapshot(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    label VARCHAR(255) NOT NULL,
+    snapshot_id VARCHAR(255) NOT NULL,
+    parent_id VARCHAR(255) NOT NULL,
+    guest_uuid CHAR(36) NOT NULL,
+    status TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    create_time BIGINT UNSIGNED NOT NULL,
+    xml TEXT NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE snapshot ADD INDEX (label);
+ALTER TABLE snapshot ADD INDEX (snapshot_id);
+ALTER TABLE snapshot ADD INDEX (guest_uuid);
+
+
+CREATE TABLE IF NOT EXISTS snapshot_disk_mapping(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    snapshot_id VARCHAR(255) NOT NULL,
+    disk_uuid CHAR(36) NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE snapshot_disk_mapping ADD UNIQUE INDEX (snapshot_id, disk_uuid);
+ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
+
+
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-Systemd', '用作 Redhat Systemd 系列的系统初始化。初始化操作依据 CentOS 7 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-SysV', '用作 Redhat SysV 系列的系统初始化。初始化操作依据 CentOS 6.8 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Gentoo-OpenRC', '用作 Gentoo OpenRC 系列的系统初始化。', 1);

+ 32 - 0
misc/v0.3_to_v0.4/update.sql

@@ -0,0 +1,32 @@
+
+
+CREATE TABLE IF NOT EXISTS snapshot(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    label VARCHAR(255) NOT NULL,
+    snapshot_id VARCHAR(255) NOT NULL,
+    parent_id VARCHAR(255) NOT NULL,
+    guest_uuid CHAR(36) NOT NULL,
+    status TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    create_time BIGINT UNSIGNED NOT NULL,
+    xml TEXT NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE snapshot ADD INDEX (label);
+ALTER TABLE snapshot ADD INDEX (snapshot_id);
+ALTER TABLE snapshot ADD INDEX (guest_uuid);
+
+
+CREATE TABLE IF NOT EXISTS snapshot_disk_mapping(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    snapshot_id VARCHAR(255) NOT NULL,
+    disk_uuid CHAR(36) NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE snapshot_disk_mapping ADD UNIQUE INDEX (snapshot_id, disk_uuid);
+ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
+

+ 9 - 2
models/__init__.py

@@ -99,6 +99,14 @@ from event_processor import (
     EventProcessor
 )
 
+from snapshot import (
+    Snapshot
+)
+
+from snapshot_disk_mapping import (
+    SnapshotDiskMapping
+)
+
 
 __author__ = 'James Iter'
 __date__ = '2017/3/21'
@@ -111,7 +119,6 @@ __all__ = [
     'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'GuestXML', 'Log', 'SSHKey', 'SSHKeyGuestMapping',
     'OSTemplateImage', 'OSTemplateProfile', 'OSTemplateInitializeOperateSet', 'OSTemplateInitializeOperate',
     'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO', 'HostCPUMemory', 'HostTraffic',
-    'HostDiskUsageIO', 'Host'
+    'HostDiskUsageIO', 'Host', 'Snapshot', 'SnapshotDiskMapping'
 ]
 
-

+ 41 - 0
models/event_processor.py

@@ -11,6 +11,7 @@ import jimit as ji
 from models import Database as db, Config, GuestCPUMemory, GuestTraffic, GuestDiskIO, SSHKeyGuestMapping
 from models import Guest
 from models import Disk
+from models import Snapshot, SnapshotDiskMapping
 from models import Log
 from models import Utils
 from models import EmitKind
@@ -33,6 +34,8 @@ class EventProcessor(object):
     guest = Guest()
     guest_migrate_info = GuestMigrateInfo()
     disk = Disk()
+    snapshot = Snapshot()
+    snapshot_disk_mapping = SnapshotDiskMapping()
     config = Config()
     config.id = 1
     guest_cpu_memory = GuestCPUMemory()
@@ -245,6 +248,44 @@ class EventProcessor(object):
                 cls.disk.get_by('uuid')
                 cls.disk.delete()
 
+        elif _object == 'snapshot':
+            if action == 'create':
+                if state == ResponseState.success.value:
+                    cls.snapshot.id = cls.message['message']['passback_parameters']['id']
+                    cls.snapshot.snapshot_id = data['snapshot_id']
+                    cls.snapshot.parent_id = data['parent_id']
+                    cls.snapshot.xml = data['xml']
+                    cls.snapshot.progress = 100
+                    cls.snapshot.update()
+
+                    disks, _ = Disk.get_by_filter(filter_str='guest_uuid:eq:' + cls.snapshot.guest_uuid)
+
+                    for disk in disks:
+                        cls.snapshot_disk_mapping.snapshot_id = cls.snapshot.snapshot_id
+                        cls.snapshot_disk_mapping.disk_uuid = disk['uuid']
+                        cls.snapshot_disk_mapping.create()
+
+                else:
+                    cls.snapshot.progress = 255
+                    cls.snapshot.update()
+
+            if action == 'delete':
+                if state == ResponseState.success.value:
+                    cls.snapshot.id = cls.message['message']['passback_parameters']['id']
+                    cls.snapshot.get()
+
+                    # 更新子快照的 parent_id 为,当前快照的 parent_id。因为当前快照已被删除。
+                    Snapshot.update_by_filter({'parent_id': cls.snapshot.parent_id},
+                                              filter_str='parent_id:eq:' + cls.snapshot.snapshot_id)
+
+                    SnapshotDiskMapping.delete_by_filter(
+                        filter_str=':'.join(['snapshot_id', 'eq', cls.snapshot.snapshot_id]))
+
+                    cls.snapshot.delete()
+
+                else:
+                    pass
+
         else:
             pass
 

+ 5 - 0
models/rules.py

@@ -11,6 +11,7 @@ __contact__ = 'james.iter.cn@gmail.com'
 __copyright__ = '(c) 2017 by James Iter.'
 
 
+# TODO: 考虑按视角归类,比如自身视角名称(uuid),外部视角名称(guest_uuid)
 class Rules(Enum):
     # 正则表达式方便校验其来自URL的参数
     REG_NUMBER = 'regex:^\d{1,17}$'
@@ -84,6 +85,10 @@ class Rules(Enum):
     BPS_WR = (int, 'bps_wr')
     INFLUENCE_CURRENT_GUEST = (bool, 'influence_current_guest')
 
+    GUEST_UUID = (basestring, 'guest_uuid', (36, 36))
+    SNAPSHOT_ID = (REG_NUMBER, 'snapshot_id')
+    SNAPSHOTS_ID = (REG_NUMBERS, 'snapshots_id')
+
     REMARK = (basestring, 'remark')
     USE_FOR = (int, 'use_for')
     LABEL = (basestring, 'label')

+ 51 - 0
models/snapshot.py

@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2018/4/10'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+class Snapshot(ORM):
+
+    _table_name = 'snapshot'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(Snapshot, self).__init__()
+        self.id = 0
+        self.label = None
+        self.snapshot_id = None
+        self.parent_id = None
+        self.guest_uuid = None
+        self.status = None
+        self.progress = None
+        self.create_time = ji.Common.tus()
+        self.xml = None
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'label': FilterFieldType.STR.value,
+            'snapshot_id': FilterFieldType.STR.value,
+            'guest_uuid': FilterFieldType.STR.value,
+            'create_time': FilterFieldType.INT.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return []
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['label', 'snapshot_id']
+

+ 41 - 0
models/snapshot_disk_mapping.py

@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2018/4/10'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+class SnapshotDiskMapping(ORM):
+
+    _table_name = 'snapshot_disk_mapping'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(SnapshotDiskMapping, self).__init__()
+        self.id = 0
+        self.snapshot_id = None
+        self.disk_uuid = None
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'snapshot_id': FilterFieldType.STR.value,
+            'disk_uuid': FilterFieldType.STR.value,
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return []
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['snapshot_id', 'disk_uuid']
+

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/jsplumb/jsplumb.min.js


+ 10 - 1
templates/layout.html

@@ -363,10 +363,16 @@
                                 <span>平台日志</span>
                             </a>
                         </li>
+                        <li>
+                            <a href="{{ url_for('v_snapshots.show') }}" title="虚拟机实例快照列表">
+                                <i class="glyph-icon icon-linecons-camera"></i>
+                                <span>实例快照</span>
+                            </a>
+                        </li>
                         <li>
                             <a href="{{ url_for('v_os_templates_image.show') }}" title="虚拟机模板镜像列表">
                                 <i class="glyph-icon icon-linecons-inbox"></i>
-                                <span>虚拟机模板镜像</span>
+                                <span>模板镜像</span>
                             </a>
                         </li>
                         <li>
@@ -471,6 +477,9 @@
         <!-- Quicksearch -->
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/jquery.quicksearch.js') }}"></script>
 
+        <!-- jsPlumb -->
+        <script type="text/javascript" src="{{ url_for('static', filename='jsplumb/jsplumb.min.js') }}"></script>
+
         <!-- JS-URL -->
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/url.js') }}"></script>
         <!-- JimV -->

+ 235 - 0
templates/snapshot_manage.html

@@ -0,0 +1,235 @@
+{% extends "layout.html" %}
+{% block head %}
+    {{ super() }}
+
+    <style type="text/css">
+
+        @media (min-width: 768px) {
+            .form-horizontal .control-label {
+                text-align: left;
+            }
+        }
+        
+        label>span {
+            color: deepskyblue;
+        }
+
+        .btn,
+        .form-group>div>div,
+        .form-control {
+            border-radius: 0;
+        }
+
+        .table {
+            font-size: 12px;
+            border-width: 1px;
+            line-height: 22px;
+        }
+
+        .table > tbody > tr > td {
+            color: #424547;
+        }
+
+        .table-bordered > tbody > tr {
+            padding-top: 10px;
+            padding-bottom: 10px;
+        }
+
+        .table-bordered > thead > tr > th,
+        .table-bordered > tbody > tr > th,
+        .table-bordered > tfoot > tr > th,
+        .table-bordered > thead > tr > td,
+        .table-bordered > tbody > tr > td,
+        .table-bordered > tfoot > tr > td {
+            border-style: solid;
+            border-width: 1px 0 0 0;
+        }
+
+        .guest-label {
+            color: #999999;
+        }
+
+        .guest-desc {
+            color: #333333;
+        }
+
+        .display_none {
+            display: none;
+        }
+
+        .snapshot_original {
+            top: 6em;
+            background-color: white;
+            border: 1px solid #346789;
+            cursor: default;
+            box-shadow: 2px 2px 19px #aaa;
+            -o-box-shadow: 2px 2px 19px #aaa;
+            -webkit-box-shadow: 2px 2px 19px #aaa;
+            -moz-box-shadow: 2px 2px 19px #aaa;
+            -moz-border-radius: 0.5em;
+            border-radius: 2em;
+            position: relative;
+            width: 7em;
+            height: 4em;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            -webkit-transition: -webkit-box-shadow 0.15s ease-in;
+            -moz-transition: -moz-box-shadow 0.15s ease-in;
+            -o-transition: -o-box-shadow 0.15s ease-in;
+            transition: box-shadow 0.15s ease-in;
+        }
+
+        .snapshot_checkpoint {
+            top: 5em;
+            background-color: white;
+            border: 1px solid #346789;
+            cursor: pointer;
+            box-shadow: 2px 2px 19px #aaa;
+            -o-box-shadow: 2px 2px 19px #aaa;
+            -webkit-box-shadow: 2px 2px 19px #aaa;
+            -moz-box-shadow: 2px 2px 19px #aaa;
+            -moz-border-radius: 0.5em;
+            border-radius: 3em;
+            position: relative;
+            width: 6em;
+            height: 6em;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            -webkit-transition: -webkit-box-shadow 0.15s ease-in;
+            -moz-transition: -moz-box-shadow 0.15s ease-in;
+            -o-transition: -o-box-shadow 0.15s ease-in;
+            transition: box-shadow 0.15s ease-in;
+        }
+
+        .snapshot_current {
+            top: 6em;
+            background-color: white;
+            border: 1px solid #346789;
+            cursor: default;
+            box-shadow: 2px 2px 19px #aaa;
+            -o-box-shadow: 2px 2px 19px #aaa;
+            -webkit-box-shadow: 2px 2px 19px #aaa;
+            -moz-box-shadow: 2px 2px 19px #aaa;
+            -moz-border-radius: 0.5em;
+            border-radius: 0;
+            position: relative;
+            width: 7em;
+            height: 4em;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            -webkit-transition: -webkit-box-shadow 0.15s ease-in;
+            -moz-transition: -moz-box-shadow 0.15s ease-in;
+            -o-transition: -o-box-shadow 0.15s ease-in;
+            transition: box-shadow 0.15s ease-in;
+        }
+
+        .snapshot_checkpoint:hover {
+            border:1px solid #123456;
+            box-shadow: 2px 2px 19px #444;
+            -o-box-shadow: 2px 2px 19px #444;
+            -webkit-box-shadow: 2px 2px 19px #444;
+            -moz-box-shadow: 2px 2px 19px #fff;
+            opacity:0.9;
+        }
+
+    </style>
+{% endblock head %}
+{% block body %}
+<script>
+    var resource_path = window.location.pathname;
+
+    /* jsPlumb 初始化时可调配的参数
+        Anchor : "BottomCenter",//端点的定位点的位置声明(锚点):left,top,bottom等
+        Anchors : [ null, null ],//多个锚点的位置声明
+        ConnectionsDetachable   : true,//连接是否可以使用鼠标默认分离
+        ConnectionOverlays  : [],//附加到每个连接的默认重叠
+        Connector : "Bezier",//要使用的默认连接器的类型:折线,流程等
+        Container : null,//设置父级的元素,一个容器
+        DoNotThrowErrors  : false,//如果请求不存在的Anchor,Endpoint或Connector,是否会抛出
+        DragOptions : { },//用于配置拖拽元素的参数
+        DropOptions : { },//用于配置元素的drop行为的参数
+        Endpoint : "Dot",//端点(锚点)的样式声明(Dot)
+        Endpoints : [ null, null ],//多个端点的样式声明(Dot)
+        EndpointOverlays : [ ],//端点的重叠
+        EndpointStyle : { fill : "#456" },//端点的css样式声明
+        EndpointStyles : [ null, null ],//同上
+        EndpointHoverStyle : null,//鼠标经过样式
+        EndpointHoverStyles : [ null, null ],//同上
+        HoverPaintStyle : null,//鼠标经过线的样式
+        LabelStyle : { color : "black" },//标签的默认样式。
+        LogEnabled : false,//是否打开jsPlumb的内部日志记录
+        Overlays : [ ],//重叠
+        MaxConnections : 1,//最大连接数
+        PaintStyle : { lineWidth : 8, stroke : "#456" },//连线样式
+        ReattachConnections : false,//是否重新连接使用鼠标分离的线
+        RenderMode : "svg",//默认渲染模式
+        Scope : "jsPlumb_DefaultScope"//范围,标识
+     */
+
+    $(document).ready(function() {
+        $('body').addClass('add-transition');
+        $('.add-page-transition').on('click', function(){
+            var transAttr = $(this).attr('data-transition');
+            $('.add-transition').attr('class', 'add-transition');
+            $('.add-transition').addClass(transAttr);
+        });
+
+        var color = "gray";
+
+        var firstInstance = jsPlumb.getInstance({
+            Connector:[ "Flowchart"],
+            DragOptions: { cursor: "pointer", zIndex: 2000 },
+            PaintStyle: { stroke: color, strokeWidth: 2 },
+            EndpointStyle: { radius: 9, fill: color}
+        });
+
+        firstInstance.setContainer("snapshot_panel");
+
+        var arrowCommon = { foldback: 0.7, fill: color, width: 16 };
+        var overlays = [
+                [ "Arrow", { location: 1 }, arrowCommon ]
+            ];
+
+        firstInstance.addEndpoint("snapshot_origin", {
+            uuid: "snapshot_origin_right",
+            anchor: "Right",
+            endpoint: "Blank",
+            maxConnections: 100});
+
+        firstInstance.addEndpoint("snapshot_01", {
+            uuid: "snapshot_01_left",
+            anchor: "Left",
+            endpoint: "Blank",
+            maxConnections: 100});
+
+        firstInstance.connect({uuids: ["snapshot_origin_right", "snapshot_01_left"], overlays: overlays});
+        // firstInstance.draggable(['snapshot_origin', 'snapshot_01']);
+    });
+
+</script>
+<div class="container" style="padding-top: 100px; width: 90%; max-width: 100%;">
+    <div class="panel">
+        <div class="panel-body">
+            <a href="javascript:history.go(-1)" class="btn btn-xs btn-default add-page-transition" data-transition="pt-page-moveFromLeft-init" style="margin-bottom: 4px; margin-left: 10px;">
+                <span class="glyph-icon icon-separator" style="transform: rotateY(-180deg);">
+                    <i class="glyph-icon icon-level-up"></i>
+                </span>
+                <span class="button-content">
+                    返回
+                </span>
+            </a>
+            <div id="snapshot_panel" class="row" style="margin-top: 10px; height: 500px; position: relative;">
+                <div class="snapshot_original" id="snapshot_origin" style="left: 8em;">原</div>
+                <div class="snapshot_checkpoint" id="snapshot_01" style="left: 16em;">快照1</div>
+                <div class="snapshot_checkpoint" id="snapshot_02" style="left: 24em;">快照2</div>
+                <div class="snapshot_checkpoint" id="snapshot_03" style="left: 32em;">快照3</div>
+                <div class="snapshot_checkpoint" id="snapshot_last" style="left: 40em;">快照4</div>
+                <div class="snapshot_current" id="snapshot_current" style="left: 48em;">当前</div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock body %}

+ 362 - 0
templates/snapshots_show.html

@@ -0,0 +1,362 @@
+{% extends "layout.html" %}
+{% block head %}
+    {{ super() }}
+    <style type="text/css">
+
+        @media (min-width: 768px) {
+            .form-horizontal .control-label {
+                text-align: left;
+            }
+        }
+
+        label>span {
+            color: deepskyblue;
+        }
+
+        .table {
+            font-size: 12px;
+            border-width: 1px;
+            line-height: 20px;
+        }
+        .table > thead > tr > th,
+        .table > tfoot > tr > th {
+            color: #999999;
+            font-weight: normal;
+            border-bottom: 0 solid #e1e6eb;
+            background-color: #F5F6FA;
+        }
+        
+        .table > tbody > tr > td {
+            color: #424547;
+        }
+
+        .table-bordered > tbody > tr {
+            padding-top: 10px;
+            padding-bottom: 10px;
+        }
+
+        .table-bordered > thead > tr > th,
+        .table-bordered > tbody > tr > th,
+        .table-bordered > tfoot > tr > th,
+        .table-bordered > thead > tr > td,
+        .table-bordered > tbody > tr > td,
+        .table-bordered > tfoot > tr > td {
+            border-style: solid;
+            border-width: 1px 0 0 0;
+        }
+
+        .btn,
+        .form-control,
+        .modal-content {
+            border-radius: 0 !important;
+        }
+
+        .btn-shortcut {
+            font-size: 12px !important;
+            padding: 0 26px;
+        }
+
+        .show {
+            display: inline-block !important;
+        }
+
+        .tr-selected td,
+        .tr-selected {
+            color: #000 !important;
+            background: #fafaff !important;
+        }
+    </style>
+
+{% endblock head %}
+{% block content %}
+
+<script type="text/javascript">
+    var page = 1;
+    var page_size = 10;
+    var keyword = '';
+    var resource_path = window.location.pathname;
+    var cur_url = resource_path;
+
+    $(document).ready(function() {
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+
+        var last_ready = null;
+        $('#content_search').keydown(function() {
+            if (last_ready !== null) {
+                clearTimeout(last_ready);
+            }
+            last_ready = setTimeout(function () {
+                keyword = $('#content_search').val();
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+                if (keyword.length > 0) {
+                    cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+                }
+                window.location.href=cur_url;
+            }, 1000);
+        });
+
+        $('#page_size').change(function () {
+            keyword = $('#content_search').val();
+            page_size = $('#page_size').val();
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+            if (keyword.length > 0) {
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+            }
+            window.location.href=cur_url;
+        });
+
+        $("thead").on('click', ".all_selector", function() {
+            if ($("thead .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tfoot").on('click', ".all_selector", function() {
+            if ($("tfoot .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tbody tr").on('click', "input[type='checkbox']", function() {
+            select_item_action();
+        });
+
+        $('#edit_label_modal').on('show.bs.modal', function (me) {
+            $('#snapshot_id').val($(me.relatedTarget).parent().parent().prev().prev().text());
+            $('#edit_label').val($(me.relatedTarget).prev().text());
+        });
+
+    });
+
+    function refresh() {
+        keyword = $('#content_search').val();
+        page = $('#pagination li.active a').text();
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+        if (keyword.length > 0) {
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+        }
+        window.location.href=cur_url;
+    }
+
+    function row_onmouseover(me) {
+        $(me).find(".edit_label_trigger").css('display','inline-flex');
+    }
+
+    function row_onmouseout(me) {
+        $(me).find(".edit_label_trigger").css('display','none');
+    }
+
+    function label_update(me) {
+        var snapshot_id = $('#snapshot_id').val();
+        var label = $('#edit_label').val();
+        $('#edit_label_modal').modal('hide');
+        $.ajax({
+            url : '/api/snapshot/' + snapshot_id,
+            type : 'PATCH',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                label: label
+            }),
+            error : function() {
+                alter_danger('快照名称更新失败!');
+            },
+            success : function() {
+                alter_success('快照名称更新成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
+
+    function get_selected_element(checked) {
+        if (checked === null) {
+            checked = true;
+        }
+
+        if (checked) {
+            return $('tbody :checked');
+        } else {
+
+            return $('tbody input:not(:checked)');
+        }
+    }
+    
+    function highlight_selected_element() {
+        var selected_element = get_selected_element(true);
+        var no_selected_element = get_selected_element(false);
+
+        selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', true);
+        });
+
+        no_selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', false);
+        });
+    }
+    
+    function shortcut_bar_enable() {
+        var selected_element = get_selected_element(true);
+
+        if (selected_element.length > 0) {
+            $('.btn-shortcut').toggleClass('disabled', false);
+        } else {
+            $('.btn-shortcut').toggleClass('disabled', true);
+        }
+    }
+
+    function select_item_action() {
+        highlight_selected_element();
+        shortcut_bar_enable();
+    }
+</script>
+<div class="panel">
+    <div class="panel-body">
+        <h3 class="title-hero" style="font-size: 24px;">
+            虚拟机实例快照
+        </h3>
+        <div>
+            <div id="datatable-row-highlight_wrapper" class="dataTables_wrapper form-inline">
+                <div class="row" style="padding: 10px 10px 10px 0; width: 100%;">
+                    <div class="col-sm-12" style="padding-right: 0;">
+                        <div id="datatable-row-highlight_filter" class="dataTables_filter" style="display: inline-block;">
+                            <input id="content_search" type="search" class="form-control" placeholder="模糊搜索..." value="{%- if keyword -%} {{ keyword }} {%- endif -%}" style="margin-left: 0; border-radius: 0;">
+                        </div>
+                        <div class="pull-right">
+                            <button class="btn btn-default" onclick="refresh()" style="border-radius: 0;"><span class="glyph-icon icon-elusive-arrows-cw"></span></button>
+                        </div>
+                    </div>
+                </div>
+                <table id="snapshots_list" class="table table-bordered table-hover" cellspacing="0" width="100%" role="grid"
+                       style="width: 100%; margin-bottom: 0; border-bottom-width: 0;">
+                <thead>
+                <tr role="row">
+                    <th style="display: none;">ID</th>
+                    <th><input class="all_selector" title="选取所有" type="checkbox"></th>
+                    <th width="180px;">名称</th>
+                    <th>进度</th>
+                    <th>状态</th>
+                    <th>所属虚拟机</th>
+                    <th>创建时间</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                {% for item in snapshots_ret.data %}
+                <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                    <td style="display: none;">{{ item.snapshot_id }}</td>
+                    <td><input title="选中" type="checkbox"></td>
+                    <td>
+                        <div>
+                            <a href="/snapshot/detail/{{ item.snapshot_id }}">{{ item.snapshot_id }}</a>
+                        </div>
+                        <div>
+                            <p style="display: inline-block;">{{ item.label }}</p>
+                            <a href="javascript:;" class="edit_label_trigger" data-toggle="modal" data-target="#edit_label_modal" style="display: none; float: right;">
+                                <span class="glyph-icon icon-elusive-pencil" style="width: 20px; height: 20px; margin-left: 10px; border-radius: 0; border: 1px solid rgb(220, 233, 255); background-color: #ffffff;"></span>
+                            </a>
+                        </div>
+                    </td>
+                    <td>{% if item.progress == 255 %}
+                        <span style="color: #990000;">创建失败</span>
+                        {% elif item.progress == 100 %}
+                        <span style="color: #00BB00;">{{ item.progress }}%</span>
+                        {% else %}
+                        <span style="color: #e5b715;">{{ item.progress }}%</span>
+                        {% endif %}
+                    </td>
+                    <td>{{ format_guest_status(item.status, 100)|safe }}</td>
+                    <td><a href="/guest/detail/{{ item.guest_uuid }}" {% if 'guest' not in item %}style="display: none"{% endif %}>
+                        {% if 'guest' in item %}
+                            {{ item.guest.label }}/{{ item.guest.remark }}
+                        {% endif %}</a>
+                    </td>
+                    <td>{{ format_datetime_by_tus(item.create_time) }}</td>
+                    <td>
+                        <div class="dropdown inline-block">
+                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#snapshot_revert_modal" onclick="$('#snapshot_id').val($(this).parent().parent().parent().children()[0].textContent)">恢复</a>
+                            <span> | </span>
+                            <a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#snapshot_delete_modal" onclick="$('#snapshot_id').val($(this).parent().parent().parent().children()[0].textContent)">删除</a>
+                        </div>
+                    </td>
+                </tr>
+                {% endfor %}
+                </tbody>
+                </table>
+
+                <table class="table table-bordered" style="border-top-width: 0; z-index: 99; position: sticky; bottom: 0;">
+                    <tfoot>
+                    <tr style="height: 70px;">
+                        <th><input type="checkbox" title="选取所有" class="all_selector"></th>
+                        <th>
+                            <div class="row">
+                                <div class="col-sm-6">
+                                </div>
+                                <div class="col-sm-3" style="font-size: 12px; padding-top: 5px; text-align: right;">
+                                    共有{{ os_templates_image_ret.paging.total }}条,每页显示:
+                                    <select id="page_size" name="datatable-row-highlight_length" title="page_size" class="form-control" style="height: 22px; vertical-align: baseline;">
+                                        <option value="10" {% if page_size == 10 %} selected {% endif %}>10</option>
+                                        <option value="20" {% if page_size == 20 %} selected {% endif %}>20</option>
+                                        <option value="50" {% if page_size == 50  %} selected {% endif %}>50</option>
+                                    </select>&nbsp;&nbsp;条
+                                </div>
+                                <div class="col-sm-3" style="text-align: left;">
+                                    <div class="dataTables_paginate paging_bootstrap" id="datatable-row-highlight_paginate">
+                                        <ul id="pagination" class="pagination">
+                                            <li class="{% if page == 1 %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page - 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">«</a>
+                                            </li>
+                                            {% for item in pages %}
+                                            <li class="{% if item == page %} active {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ item }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">{{ item }}</a>
+                                            </li>
+                                            {% endfor %}
+                                            <li class="{% if page == last_page %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page + 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">»</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </th>
+                    </tr>
+                    </tfoot>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<input id="snapshot_id" title="快照 ID" class="form-control" name="snapshot_id" hidden>
+
+<div class="modal" id="edit_label_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">编辑实例快照名称:</h4>
+            </div>
+            <div class="modal-body">
+                <input id="edit_label" title="实例快照名称" class="form-control" name="snapshot_label">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="label_update();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock content %}

+ 0 - 5
views/guest.py

@@ -84,11 +84,6 @@ def show():
     for os_template_profile in os_templates_profile_ret['data']:
         os_templates_profile_mapping_by_id[os_template_profile['id']] = os_template_profile
 
-    guests_uuid = list()
-
-    for guest in guests_ret['data']:
-        guests_uuid.append(guest['uuid'])
-
     last_page = int(ceil(guests_ret['paging']['total'] / float(page_size)))
     page_length = 5
     pages = list()

+ 0 - 4
views/os_template_image.py

@@ -34,7 +34,6 @@ def show():
     keyword = request.args.get('keyword', None)
     order_by = request.args.get('order_by', None)
     order = request.args.get('order', None)
-    filters = list()
 
     if page is not None:
         args.append('page=' + page.__str__())
@@ -51,9 +50,6 @@ def show():
     if order is not None:
         args.append('order=' + order)
 
-    if filters.__len__() > 0:
-        args.append('filter=' + ','.join(filters))
-
     host_url = request.host_url.rstrip('/')
 
     os_templates_image_url = host_url + url_for('api_os_templates_image.r_get_by_filter')

+ 109 - 0
views/snapshot.py

@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+from flask import Blueprint, render_template, url_for, request, redirect
+import requests
+from math import ceil
+
+
+__author__ = 'James Iter'
+__date__ = '2018/3/25'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'v_snapshot',
+    __name__,
+    url_prefix='/snapshot'
+)
+
+blueprints = Blueprint(
+    'v_snapshots',
+    __name__,
+    url_prefix='/snapshots'
+)
+
+
+def show():
+    args = list()
+    page = int(request.args.get('page', 1))
+    page_size = int(request.args.get('page_size', 10))
+    keyword = request.args.get('keyword', None)
+    order_by = request.args.get('order_by', None)
+    order = request.args.get('order', None)
+
+    if page is not None:
+        args.append('page=' + page.__str__())
+
+    if page_size is not None:
+        args.append('page_size=' + page_size.__str__())
+
+    if keyword is not None:
+        args.append('keyword=' + keyword.__str__())
+
+    if order_by is not None:
+        args.append('order_by=' + order_by)
+
+    if order is not None:
+        args.append('order=' + order)
+
+    host_url = request.host_url.rstrip('/')
+    snapshots_url = host_url + url_for('api_snapshots.r_get_by_filter')
+    if keyword is not None:
+        snapshots_url = host_url + url_for('api_snapshots.r_content_search')
+
+    if args.__len__() > 0:
+        snapshots_url = snapshots_url + '?' + '&'.join(args)
+
+    snapshots_ret = requests.get(url=snapshots_url, cookies=request.cookies)
+    snapshots_ret = json.loads(snapshots_ret.content)
+
+    guests_uuid = list()
+
+    for snapshot in snapshots_ret['data']:
+        guests_uuid.append(snapshot['guest_uuid'])
+
+    guests_url = host_url + url_for('api_guests.r_get_by_filter', filter='uuid:in:' + ','.join(guests_uuid))
+
+    guests_ret = requests.get(url=guests_url, cookies=request.cookies)
+    guests_ret = json.loads(guests_ret.content)
+
+    # Guest uuid 与 Guest 的映射
+    guests_mapping_by_uuid = dict()
+    for guest in guests_ret['data']:
+        guests_mapping_by_uuid[guest['uuid']] = guest
+
+    for i, snapshot in enumerate(snapshots_ret['data']):
+        if snapshot['guest_uuid'].__len__() == 36:
+            snapshots_ret['data'][i]['guest'] = guests_mapping_by_uuid[snapshot['guest_uuid']]
+
+    last_page = int(ceil(1 / float(page_size)))
+    page_length = 5
+    pages = list()
+    if page < int(ceil(page_length / 2.0)):
+        for i in range(1, page_length + 1):
+            pages.append(i)
+            if i == last_page or last_page == 0:
+                break
+
+    elif last_page - page < page_length / 2:
+        for i in range(last_page - page_length + 1, last_page + 1):
+            if i < 1:
+                continue
+            pages.append(i)
+
+    else:
+        for i in range(page - page_length / 2, page + int(ceil(page_length / 2.0))):
+            pages.append(i)
+            if i == last_page or last_page == 0:
+                break
+
+    return render_template('snapshots_show.html',
+                           page=page, page_size=page_size, keyword=keyword, pages=pages, order_by=order_by, order=order,
+                           last_page=last_page, snapshots_ret=snapshots_ret,
+                           guests_mapping_by_uuid=guests_mapping_by_uuid)
+
+

+ 0 - 4
views/ssh_key.py

@@ -34,7 +34,6 @@ def show():
     keyword = request.args.get('keyword', None)
     order_by = request.args.get('order_by', None)
     order = request.args.get('order', None)
-    filters = list()
 
     if page is not None:
         args.append('page=' + page.__str__())
@@ -51,9 +50,6 @@ def show():
     if order is not None:
         args.append('order=' + order)
 
-    if filters.__len__() > 0:
-        args.append('filter=' + ','.join(filters))
-
     host_url = request.host_url.rstrip('/')
 
     ssh_keys_url = host_url + url_for('api_ssh_keys.r_get_by_filter')

+ 2 - 0
views_route_table.py

@@ -12,6 +12,7 @@ from views import config
 from views import misc
 from views import os_template_image
 from views import ssh_key
+from views import snapshot
 
 
 __author__ = 'James Iter'
@@ -51,3 +52,4 @@ add_rule_views(host.blueprint, '/detail/<node_id>', views_func='host.detail', me
 add_rule_views(ssh_key.blueprints, '', views_func='ssh_key.show', methods=['GET'])
 add_rule_views(ssh_key.blueprint, '', views_func='ssh_key.create', methods=['POST'])
 
+add_rule_views(snapshot.blueprints, '', views_func='snapshot.show', methods=['GET'])

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff