James Iter 8 лет назад
Родитель
Сommit
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 user
 from api import guest
 from api import guest
 from api import disk
 from api import disk
+from api import snapshot
 from api import os_template_image
 from api import os_template_image
 from api import os_template_profile
 from api import os_template_profile
 from api import os_template_initialize_operate_set
 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, '/_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(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 性能查询
 # Guest 性能查询
 add_rule_api(guest_performance.blueprint, '/cpu_memory',
 add_rule_api(guest_performance.blueprint, '/cpu_memory',

+ 1 - 0
docs/todo.md

@@ -79,5 +79,6 @@
 - [x] 取消单独的初始化密码操作,合并入具体的操作系统初始化操作中
 - [x] 取消单独的初始化密码操作,合并入具体的操作系统初始化操作中
 - [ ] 迁移中的虚拟机,不允许做任何操作
 - [ ] 迁移中的虚拟机,不允许做任何操作
 - [ ] 通过 QemuGuestAgent 实现 Guest 的内存使用率监控
 - [ ] 通过 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.host import blueprints as host_blueprints
 from api.ssh_key import blueprint as ssh_key_blueprint
 from api.ssh_key import blueprint as ssh_key_blueprint
 from api.ssh_key import blueprints as ssh_key_blueprints
 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 blueprint as performance_blueprint
 from api.guest_performance import blueprints as performance_blueprints
 from api.guest_performance import blueprints as performance_blueprints
 from api.host_performance import blueprint as host_performance_blueprint
 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.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 blueprint as view_ssh_key_blueprint
 from views.ssh_key import blueprints as view_ssh_key_blueprints
 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 blueprint as view_host_blueprint
 from views.host import blueprints as view_host_blueprints
 from views.host import blueprints as view_host_blueprints
@@ -235,6 +239,8 @@ try:
     app.register_blueprint(host_blueprints)
     app.register_blueprint(host_blueprints)
     app.register_blueprint(ssh_key_blueprint)
     app.register_blueprint(ssh_key_blueprint)
     app.register_blueprint(ssh_key_blueprints)
     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_blueprint)
     app.register_blueprint(performance_blueprints)
     app.register_blueprint(performance_blueprints)
     app.register_blueprint(host_performance_blueprint)
     app.register_blueprint(host_performance_blueprint)
@@ -253,6 +259,8 @@ try:
     app.register_blueprint(view_os_template_image_blueprints)
     app.register_blueprint(view_os_template_image_blueprints)
     app.register_blueprint(view_ssh_key_blueprint)
     app.register_blueprint(view_ssh_key_blueprint)
     app.register_blueprint(view_ssh_key_blueprints)
     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_blueprint)
     app.register_blueprint(view_host_blueprints)
     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);
 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-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 ('CentOS-SysV', '用作 Redhat SysV 系列的系统初始化。初始化操作依据 CentOS 6.8 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Gentoo-OpenRC', '用作 Gentoo OpenRC 系列的系统初始化。', 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
     EventProcessor
 )
 )
 
 
+from snapshot import (
+    Snapshot
+)
+
+from snapshot_disk_mapping import (
+    SnapshotDiskMapping
+)
+
 
 
 __author__ = 'James Iter'
 __author__ = 'James Iter'
 __date__ = '2017/3/21'
 __date__ = '2017/3/21'
@@ -111,7 +119,6 @@ __all__ = [
     'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'GuestXML', 'Log', 'SSHKey', 'SSHKeyGuestMapping',
     'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'GuestXML', 'Log', 'SSHKey', 'SSHKeyGuestMapping',
     'OSTemplateImage', 'OSTemplateProfile', 'OSTemplateInitializeOperateSet', 'OSTemplateInitializeOperate',
     'OSTemplateImage', 'OSTemplateProfile', 'OSTemplateInitializeOperateSet', 'OSTemplateInitializeOperate',
     'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO', 'HostCPUMemory', 'HostTraffic',
     '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 Database as db, Config, GuestCPUMemory, GuestTraffic, GuestDiskIO, SSHKeyGuestMapping
 from models import Guest
 from models import Guest
 from models import Disk
 from models import Disk
+from models import Snapshot, SnapshotDiskMapping
 from models import Log
 from models import Log
 from models import Utils
 from models import Utils
 from models import EmitKind
 from models import EmitKind
@@ -33,6 +34,8 @@ class EventProcessor(object):
     guest = Guest()
     guest = Guest()
     guest_migrate_info = GuestMigrateInfo()
     guest_migrate_info = GuestMigrateInfo()
     disk = Disk()
     disk = Disk()
+    snapshot = Snapshot()
+    snapshot_disk_mapping = SnapshotDiskMapping()
     config = Config()
     config = Config()
     config.id = 1
     config.id = 1
     guest_cpu_memory = GuestCPUMemory()
     guest_cpu_memory = GuestCPUMemory()
@@ -245,6 +248,44 @@ class EventProcessor(object):
                 cls.disk.get_by('uuid')
                 cls.disk.get_by('uuid')
                 cls.disk.delete()
                 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:
         else:
             pass
             pass
 
 

+ 5 - 0
models/rules.py

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

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/jsplumb/jsplumb.min.js


+ 10 - 1
templates/layout.html

@@ -363,10 +363,16 @@
                                 <span>平台日志</span>
                                 <span>平台日志</span>
                             </a>
                             </a>
                         </li>
                         </li>
+                        <li>
+                            <a href="{{ url_for('v_snapshots.show') }}" title="虚拟机实例快照列表">
+                                <i class="glyph-icon icon-linecons-camera"></i>
+                                <span>实例快照</span>
+                            </a>
+                        </li>
                         <li>
                         <li>
                             <a href="{{ url_for('v_os_templates_image.show') }}" title="虚拟机模板镜像列表">
                             <a href="{{ url_for('v_os_templates_image.show') }}" title="虚拟机模板镜像列表">
                                 <i class="glyph-icon icon-linecons-inbox"></i>
                                 <i class="glyph-icon icon-linecons-inbox"></i>
-                                <span>虚拟机模板镜像</span>
+                                <span>模板镜像</span>
                             </a>
                             </a>
                         </li>
                         </li>
                         <li>
                         <li>
@@ -471,6 +477,9 @@
         <!-- Quicksearch -->
         <!-- Quicksearch -->
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/jquery.quicksearch.js') }}"></script>
         <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 -->
         <!-- JS-URL -->
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/url.js') }}"></script>
         <script type="text/javascript" src="{{ url_for('static', filename='js-core/url.js') }}"></script>
         <!-- JimV -->
         <!-- 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']:
     for os_template_profile in os_templates_profile_ret['data']:
         os_templates_profile_mapping_by_id[os_template_profile['id']] = os_template_profile
         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)))
     last_page = int(ceil(guests_ret['paging']['total'] / float(page_size)))
     page_length = 5
     page_length = 5
     pages = list()
     pages = list()

+ 0 - 4
views/os_template_image.py

@@ -34,7 +34,6 @@ def show():
     keyword = request.args.get('keyword', None)
     keyword = request.args.get('keyword', None)
     order_by = request.args.get('order_by', None)
     order_by = request.args.get('order_by', None)
     order = request.args.get('order', None)
     order = request.args.get('order', None)
-    filters = list()
 
 
     if page is not None:
     if page is not None:
         args.append('page=' + page.__str__())
         args.append('page=' + page.__str__())
@@ -51,9 +50,6 @@ def show():
     if order is not None:
     if order is not None:
         args.append('order=' + order)
         args.append('order=' + order)
 
 
-    if filters.__len__() > 0:
-        args.append('filter=' + ','.join(filters))
-
     host_url = request.host_url.rstrip('/')
     host_url = request.host_url.rstrip('/')
 
 
     os_templates_image_url = host_url + url_for('api_os_templates_image.r_get_by_filter')
     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)
     keyword = request.args.get('keyword', None)
     order_by = request.args.get('order_by', None)
     order_by = request.args.get('order_by', None)
     order = request.args.get('order', None)
     order = request.args.get('order', None)
-    filters = list()
 
 
     if page is not None:
     if page is not None:
         args.append('page=' + page.__str__())
         args.append('page=' + page.__str__())
@@ -51,9 +50,6 @@ def show():
     if order is not None:
     if order is not None:
         args.append('order=' + order)
         args.append('order=' + order)
 
 
-    if filters.__len__() > 0:
-        args.append('filter=' + ','.join(filters))
-
     host_url = request.host_url.rstrip('/')
     host_url = request.host_url.rstrip('/')
 
 
     ssh_keys_url = host_url + url_for('api_ssh_keys.r_get_by_filter')
     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 misc
 from views import os_template_image
 from views import os_template_image
 from views import ssh_key
 from views import ssh_key
+from views import snapshot
 
 
 
 
 __author__ = 'James Iter'
 __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.blueprints, '', views_func='ssh_key.show', methods=['GET'])
 add_rule_views(ssh_key.blueprint, '', views_func='ssh_key.create', methods=['POST'])
 add_rule_views(ssh_key.blueprint, '', views_func='ssh_key.create', methods=['POST'])
 
 
+add_rule_views(snapshot.blueprints, '', views_func='snapshot.show', methods=['GET'])

Некоторые файлы не были показаны из-за большого количества измененных файлов