Преглед на файлове

走通从快照导出模板流程

James Iter преди 8 години
родител
ревизия
51b7124456

+ 1 - 1
README.md

@@ -24,7 +24,7 @@
 
 ## 项目描述
 
-> 计算机硬件越来越白菜价,性能越来越强劲,企业电子信息化方面的业务越来越多,"互联网+"、大数据的浪潮已经掀起,物联网、AI的趋势正在形成。
+> 计算机硬件越趋便宜,性能更为强劲,企业电子信息化方面的业务加重,"互联网+"、大数据的浪潮已经掀起,物联网、AI的趋势正在形成。
 > 因为上述的一切,虚拟化技术被处于一个软化硬件,揉和硬件与业务系统这么一个核心角色。
 > 虚拟化技术虽然已经被普及了很久,但多数企业依然仅仅是把它当做独立的虚拟硬件来使用。在资源的科学分配、高效利用、自动化管理方面,还差些许。
 > JimV 是一个,结构清晰简单,易于部署、维护、使用的,低门槛企业私有云管理平台。

+ 10 - 1
api/os_template_image.py

@@ -46,7 +46,8 @@ def r_create():
         Rules.PATH.value,
         Rules.LOGO.value,
         Rules.OS_TEMPLATE_PROFILE_ID_EXT.value,
-        Rules.ACTIVE.value
+        Rules.ACTIVE.value,
+        Rules.OS_TEMPLATE_IMAGE_KIND.value
     ]
 
     os_template_image.label = request.json.get('label')
@@ -55,6 +56,8 @@ def r_create():
     os_template_image.logo = request.json.get('logo')
     os_template_image.active = bool(int(request.json.get('active', 1)))
     os_template_image.os_template_profile_id = request.json.get('os_template_profile_id')
+    os_template_image.kind = request.json.get('kind')
+    os_template_image.progress = 100
 
     try:
         ji.Check.previewing(args_rules, os_template_image.__dict__)
@@ -122,6 +125,11 @@ def r_update(_id):
             Rules.OS_TEMPLATE_PROFILE_ID_EXT.value,
         )
 
+    if 'kind' in request.json:
+        args_rules.append(
+            Rules.OS_TEMPLATE_IMAGE_KIND.value,
+        )
+
     if args_rules.__len__() < 2:
         ret = dict()
         ret['state'] = ji.Common.exchange_state(20000)
@@ -141,6 +149,7 @@ def r_update(_id):
         os_template_image.logo = request.json.get('logo', os_template_image.logo)
         os_template_image.os_template_profile_id = \
             request.json.get('os_template_profile_id', os_template_image.os_template_profile_id)
+        os_template_image.kind = request.json.get('kind', os_template_image.kind)
 
         os_template_image.update()
         os_template_image.get()

+ 119 - 1
api/snapshot.py

@@ -6,12 +6,15 @@ from flask import Blueprint
 from flask import request
 import json
 import jimit as ji
+import os
 
 from api.base import Base
-from models import Guest
+from models import Guest, Config, Disk
 from models import Snapshot, SnapshotDiskMapping
+from models import OSTemplateImage
 from models import Utils
 from models import Rules
+from models import OSTemplateImageKind
 
 
 __author__ = 'James Iter'
@@ -239,3 +242,118 @@ def r_revert(snapshot_id):
     except ji.PreviewingError, e:
         return json.loads(e.message)
 
+
+@Utils.dumps2response
+def r_get_disks(snapshot_id):
+
+    args_rules = [
+        Rules.SNAPSHOT_ID.value
+    ]
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = list()
+
+        ji.Check.previewing(args_rules, {'snapshot_id': snapshot_id})
+
+        rows, _ = SnapshotDiskMapping.get_by_filter(filter_str=':'.join(['snapshot_id', 'eq', snapshot_id]))
+
+        for row in rows:
+            ret['data'].append(row['disk_uuid'])
+
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_convert_to_os_template_image(snapshot_id, disk_uuid):
+
+    args_rules = [
+        Rules.SNAPSHOT_ID.value,
+        Rules.DISK_UUID.value,
+        Rules.LABEL.value
+    ]
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, {'snapshot_id': snapshot_id, 'disk_uuid': disk_uuid,
+                                         'label': request.json.get('label')})
+
+        rows, _ = SnapshotDiskMapping.get_by_filter(filter_str=':'.join(['snapshot_id', 'eq', snapshot_id]))
+
+        disks_uuid = list()
+
+        for row in rows:
+            disks_uuid.append(row['disk_uuid'])
+
+        if disk_uuid not in disks_uuid:
+            ret['state'] = ji.Common.exchange_state(40401)
+            ret['state']['sub']['zh-cn'] = ''.join([ret['state']['sub']['zh-cn'], u': 未在快照: ',
+                                                    snapshot_id, u' 中找到磁盘:', disk_uuid])
+            return ret
+
+        config = Config()
+        config.id = 1
+        config.get()
+
+        snapshot = Snapshot()
+        os_template_image = OSTemplateImage()
+        guest = Guest()
+        disk = Disk()
+
+        snapshot.snapshot_id = snapshot_id
+        snapshot.get_by('snapshot_id')
+
+        guest.uuid = snapshot.guest_uuid
+        guest.get_by('uuid')
+
+        disk.uuid = disk_uuid
+        disk.get_by('uuid')
+
+        os_template_image.id = guest.os_template_image_id
+        os_template_image.get()
+
+        image_name = '_'.join([snapshot.snapshot_id, disk.uuid]) + '.' + disk.format
+
+        os_template_image.id = 0
+        os_template_image.label = request.json.get('label')
+        os_template_image.path = '/'.join([os.path.dirname(os_template_image.path), image_name])
+        os_template_image.kind = OSTemplateImageKind.custom.value
+        os_template_image.progress = 0
+        os_template_image.create_time = ji.Common.tus()
+
+        if os_template_image.exist_by('path'):
+            ret['state'] = ji.Common.exchange_state(40901)
+            ret['state']['sub']['zh-cn'] = ''.join([ret['state']['sub']['zh-cn'], ': ', os_template_image.path])
+            return ret
+
+        os_template_image.create()
+        os_template_image.get_by('path')
+
+        message = {
+            '_object': 'snapshot',
+            'action': 'convert',
+            'uuid': disk.guest_uuid,
+            'snapshot_id': snapshot.snapshot_id,
+            'storage_mode': config.storage_mode,
+            'dfs_volume': config.dfs_volume,
+            'node_id': disk.node_id,
+            'snapshot_path': disk.path,
+            'template_path': os_template_image.path,
+            'os_template_image_id': os_template_image.id,
+            'passback_parameters': {'id': os_template_image.id}
+        }
+
+        Utils.emit_instruction(message=json.dumps(message, ensure_ascii=False))
+
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+

+ 3 - 0
api_route_table.py

@@ -150,6 +150,9 @@ add_rule_api(snapshot.blueprints, '/<snapshots_id>', api_func='snapshot.r_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'])
 add_rule_api(snapshot.blueprint, '/_revert/<snapshot_id>', api_func='snapshot.r_revert', methods=['PUT'])
+add_rule_api(snapshot.blueprint, '/_disks/<snapshot_id>', api_func='snapshot.r_get_disks', methods=['GET'])
+add_rule_api(snapshot.blueprint, '/_convert_to_os_template_image/<snapshot_id>/<disk_uuid>',
+             api_func='snapshot.r_convert_to_os_template_image', methods=['PUT'])
 
 # 日志查询
 # Guest 性能查询

+ 3 - 0
misc/init.sql

@@ -121,6 +121,9 @@ CREATE TABLE IF NOT EXISTS os_template_image(
     active BOOLEAN NOT NULL DEFAULT TRUE,
     logo VARCHAR(255) NOT NULL,
     os_template_profile_id BIGINT UNSIGNED NOT NULL,
+    kind TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    create_time BIGINT UNSIGNED NOT NULL,
     PRIMARY KEY (id))
     ENGINE=InnoDB
     DEFAULT CHARSET=utf8;

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

@@ -30,3 +30,7 @@ CREATE TABLE IF NOT EXISTS snapshot_disk_mapping(
 ALTER TABLE snapshot_disk_mapping ADD UNIQUE INDEX (snapshot_id, disk_uuid);
 ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
 
+ALTER TABLE os_template_image ADD COLUMN kind TINYINT UNSIGNED NOT NULL DEFAULT 0;
+ALTER TABLE os_template_image ADD COLUMN progress TINYINT UNSIGNED NOT NULL DEFAULT 0;
+ALTER TABLE os_template_image ADD COLUMN create_time BIGINT UNSIGNED NOT NULL;
+

+ 3 - 2
models/__init__.py

@@ -68,7 +68,8 @@ from status import (
     GuestState,
     ResponseState,
     DiskState,
-    LogLevel
+    LogLevel,
+    OSTemplateImageKind
 )
 
 from guest_xml import (
@@ -119,6 +120,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', 'Snapshot', 'SnapshotDiskMapping'
+    'HostDiskUsageIO', 'Host', 'Snapshot', 'SnapshotDiskMapping', 'OSTemplateImageKind'
 ]
 

+ 26 - 1
models/event_processor.py

@@ -11,7 +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 Snapshot, SnapshotDiskMapping, OSTemplateImage
 from models import Log
 from models import Utils
 from models import EmitKind
@@ -36,6 +36,7 @@ class EventProcessor(object):
     disk = Disk()
     snapshot = Snapshot()
     snapshot_disk_mapping = SnapshotDiskMapping()
+    os_template_image = OSTemplateImage()
     config = Config()
     config.id = 1
     guest_cpu_memory = GuestCPUMemory()
@@ -110,6 +111,18 @@ class EventProcessor(object):
 
             cls.guest.progress = cls.message['message']['progress']
 
+        elif cls.guest.status == GuestState.snapshot_converting.value:
+            print cls.message
+            cls.os_template_image.id = cls.message['message']['os_template_image_id']
+            cls.os_template_image.get()
+
+            if cls.message['message']['progress'] <= cls.os_template_image.progress:
+                return
+
+            cls.os_template_image.progress = cls.message['message']['progress']
+            cls.os_template_image.update()
+            return
+
         cls.guest.update()
 
         # 限定特殊情况下更新磁盘所属 Guest,避免迁移、创建时频繁被无意义的更新
@@ -295,6 +308,18 @@ class EventProcessor(object):
                 cls.snapshot.progress = 100
                 cls.snapshot.update()
 
+            if action == 'convert':
+                cls.os_template_image.id = cls.message['message']['passback_parameters']['id']
+                cls.os_template_image.get()
+
+                if state == ResponseState.success.value:
+                    cls.os_template_image.progress = 100
+
+                else:
+                    cls.os_template_image.progress = 255
+
+                cls.os_template_image.update()
+
         else:
             pass
 

+ 8 - 1
models/os_template_image.py

@@ -2,6 +2,8 @@
 # -*- coding: utf-8 -*-
 
 
+import jimit as ji
+
 from models import FilterFieldType
 from models import ORM
 
@@ -26,6 +28,9 @@ class OSTemplateImage(ORM):
         self.os_template_profile_id = None
         self.path = None
         self.active = True
+        self.kind = None
+        self.progress = None
+        self.create_time = ji.Common.tus()
 
     @staticmethod
     def get_filter_keywords():
@@ -34,7 +39,9 @@ class OSTemplateImage(ORM):
             'label': FilterFieldType.STR.value,
             'path': FilterFieldType.STR.value,
             'os_template_profile_id': FilterFieldType.INT.value,
-            'active': FilterFieldType.INT.value
+            'active': FilterFieldType.INT.value,
+            'kind': FilterFieldType.INT.value,
+            'progress': FilterFieldType.INT.value
         }
 
     @staticmethod

+ 1 - 0
models/rules.py

@@ -103,6 +103,7 @@ class Rules(Enum):
     ICON = (basestring, 'icon')
     LOGO = (basestring, 'logo')
 
+    OS_TEMPLATE_IMAGE_KIND = (int, 'kind')
     OS_TEMPLATE_PROFILE_ID_EXT = (int, 'os_template_profile_id')
     OS_TEMPLATE_INITIALIZE_OPERATE_SET_ID_EXT = (int, 'os_template_initialize_operate_set_id')
     OS_TEMPLATE_INITIALIZE_OPERATE_KIND = (int, 'kind')

+ 6 - 0
models/status.py

@@ -47,6 +47,7 @@ class GuestState(IntEnum):
     migrating = 8
     update = 9
     creating = 10
+    snapshot_converting = 11
     dirty = 255
 
 
@@ -91,6 +92,11 @@ class OSTemplateInitializeOperateKind(IntEnum):
     append_file = 2
 
 
+class OSTemplateImageKind(IntEnum):
+    public = 0
+    custom = 1
+
+
 class GuestCollectionPerformanceDataKind(IntEnum):
     cpu_memory = 0
     traffic = 1

+ 1 - 1
templates/guest_create.html

@@ -133,7 +133,7 @@
 
     function refresh_os_template_image_id_selectpicker(selectpicker) {
         $.ajax({
-            url : '/api/os_templates_image',
+            url : '/api/os_templates_image?filter=active:eq:1',
             type : 'GET',
             contentType: "application/json; charset=utf-8",
             dataType: 'json',

+ 279 - 120
templates/os_templates_image_show.html

@@ -227,6 +227,19 @@
         window.location.href=cur_url;
     }
 
+    function select_this(me) {
+        $(me).parent().parent().children().removeClass('active');
+        $(me).parent().addClass('active');
+
+        $('#public_os_templates_image, #custom_os_templates_image').css('display', 'none');
+
+        if (me.id === 'custom_os_templates_image_label') {
+            $('#custom_os_templates_image').css('display', 'unset');
+        } else {
+            $('#public_os_templates_image').css('display', 'unset');
+        }
+    }
+
     function row_onmouseover(me) {
         $(me).find(".edit_label_trigger, .edit_path_trigger").css('display','inline-flex');
     }
@@ -490,24 +503,24 @@
 </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>
-                            <a class="btn btn-info" href="javascript:;" data-toggle="modal" data-target="#add_os_template_image_modal" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">添加模板</a>
-                        </div>
+        <ul class="nav nav-tabs mrg25B">
+            <li class="active"><a href="javascript:;" onclick="select_this(this);" id="public_os_templates_image_label">公共镜像</a></li>
+            <li><a href="javascript:;" onclick="select_this(this);" id="custom_os_templates_image_label">自定义镜像</a></li>
+        </ul>
+        <div id="public_os_templates_image" 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>
+                        <a class="btn btn-info" href="javascript:;" data-toggle="modal" data-target="#add_os_template_image_modal" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">添加模板</a>
                     </div>
                 </div>
-                <table id="os_templates_image_list" class="table table-bordered table-hover" cellspacing="0" width="100%" role="grid"
-                       style="width: 100%; margin-bottom: 0; border-bottom-width: 0;">
+            </div>
+            <table id="os_templates_image_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>
@@ -520,123 +533,269 @@
                 </tr>
                 </thead>
                 <tbody>
-                {% for item in os_templates_image_ret.data %}
-                <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
-                    <td style="display: none;">{{ item.id }}</td>
-                    <td><input title="选中" type="checkbox"></td>
-                    <td>
-                        <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.active == 0 %}
-                        <span style="color: #990000;">未启用</span>
+                {% for item in os_templates_image_ret.data if item.kind == 0 %}
+                    <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                        <td style="display: none;">{{ item.id }}</td>
+                        <td><input title="选中" type="checkbox"></td>
+                        <td>
+                            <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.active == 0 %}
+                            <span style="color: #990000;">未启用</span>
                         {% else %}
-                        <span style="color: #00BB00;">启用</span>
+                            <span style="color: #00BB00;">启用</span>
                         {% endif %}
-                    </td>
-                    <td>
-                        <div>
-                            <p style="display: inline-block;">{{ item.path }}</p>
-                            <a href="javascript:;" class="edit_path_trigger" data-toggle="modal" data-target="#edit_path_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>
-                        <div>
-                            <a href="javascript:;" data-toggle="modal" data-target="#update_logo_modal">
-                                {% if item.logo == "" %}
-                                    <span class="{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].icon }}"></span>
-                                {% else %}
-                                    <span class="{{ item.logo }}"></span>
-                                {% endif %}
-                            </a>
-                            <p style="display: inline-block;">{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].os_product_name }}</p>
-                        </div>
-                    </td>
-                    <td>
-                        <div class="dropdown inline-block">
-                            <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
-                                更多
-                            </a>
-                            <ul class="dropdown-menu">
-                                <li class="{% if item.active == true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
-                                    <a href="javascript:;" onclick="enable_at(this);">
-                                        启用
-                                    </a>
-                                </li>
-                                <li class="{% if item.active != true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
-                                    <a href="javascript:;" onclick="disable_at(this);">
-                                        禁用
-                                    </a>
-                                </li>
-                                <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
-                                <li>
-                                    <a href="javascript:;" data-toggle="modal" data-target="#update_os_template_image_profile_id_modal"
-                                        onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
+                        </td>
+                        <td>
+                            <div>
+                                <p style="display: inline-block;">{{ item.path }}</p>
+                                <a href="javascript:;" class="edit_path_trigger" data-toggle="modal" data-target="#edit_path_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>
+                            <div>
+                                <a href="javascript:;" data-toggle="modal" data-target="#update_logo_modal">
+                                    {% if item.logo == "" %}
+                                        <span class="{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].icon }}"></span>
+                                    {% else %}
+                                        <span class="{{ item.logo }}"></span>
+                                    {% endif %}
+                                </a>
+                                <p style="display: inline-block;">{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].os_product_name }}</p>
+                            </div>
+                        </td>
+                        <td>
+                            <div class="dropdown inline-block">
+                                <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
+                                    更多
+                                </a>
+                                <ul class="dropdown-menu">
+                                    <li class="{% if item.active == true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                        <a href="javascript:;" onclick="enable_at(this);">
+                                            启用
+                                        </a>
+                                    </li>
+                                    <li class="{% if item.active != true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                        <a href="javascript:;" onclick="disable_at(this);">
+                                            禁用
+                                        </a>
+                                    </li>
+                                    <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                    <li>
+                                        <a href="javascript:;" data-toggle="modal" data-target="#update_os_template_image_profile_id_modal"
+                                           onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
                                         $('#update_os_template_image_profile_id_instance_desc').text($(this).parent().parent().parent().parent().parent().children()[2].textContent)">
-                                        变更发行版本
-                                    </a>
-                                </li>
-                                <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
-                                <li>
-                                    <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
-                                       onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
+                                            变更发行版本
+                                        </a>
+                                    </li>
+                                    <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                    <li>
+                                        <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                           onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
                                        $('#delete_instance_desc').text($(this).parent().parent().parent().parent().parent().children()[2].textContent)">
-                                        删除
-                                    </a>
-                                </li>
-                            </ul>
+                                            删除
+                                        </a>
+                                    </li>
+                                </ul>
+                            </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>
-                    </td>
+                    </th>
+                </tr>
+                </tfoot>
+            </table>
+        </div>
+        <div id="custom_os_templates_image" class="dataTables_wrapper form-inline" style="display: none;">
+            <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="os_templates_image_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="220px;">名称</th>
+                    <th>状态</th>
+                    <th>进度</th>
+                    <th>创建时间</th>
+                    <th>发行版本</th>
+                    <th>操作</th>
                 </tr>
+                </thead>
+                <tbody>
+                {% for item in os_templates_image_ret.data if item.kind == 1 %}
+                    <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                        <td style="display: none;">{{ item.id }}</td>
+                        <td><input title="选中" type="checkbox"></td>
+                        <td>
+                            <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.active == 0 %}
+                            <span style="color: #990000;">未启用</span>
+                        {% else %}
+                            <span style="color: #00BB00;">启用</span>
+                        {% endif %}
+                        </td>
+                        <td>
+                            {% if item.progress == 255 %}
+                                <span style="color: #990000;">创建失败</span>
+                            {% elif item.progress == 254 %}
+                                <span style="color: #ff0000dd;">删除中...</span>
+                            {% elif item.progress == 100 %}
+                                <span style="color: #00BB00;">{{ item.progress }}%</span>
+                            {% else %}
+                                <span style="color: #e5b715;">{{ item.progress }}%</span>
+                            {% endif %}
+                        </td>
+                        <td>{{ format_datetime_by_tus(item.create_time) }}</td>
+                        <td>
+                            <div>
+                                <a href="javascript:;" data-toggle="modal" data-target="#update_logo_modal">
+                                    {% if item.logo == "" %}
+                                        <span class="{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].icon }}"></span>
+                                    {% else %}
+                                        <span class="{{ item.logo }}"></span>
+                                    {% endif %}
+                                </a>
+                                <p style="display: inline-block;">{{ os_templates_profile_mapping_by_id[item.os_template_profile_id].os_product_name }}</p>
+                            </div>
+                        </td>
+                        <td>
+                            <div class="dropdown inline-block">
+                                <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
+                                    更多
+                                </a>
+                                <ul class="dropdown-menu">
+                                    <li class="{% if item.active == true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                        <a href="javascript:;" onclick="enable_at(this);">
+                                            启用
+                                        </a>
+                                    </li>
+                                    <li class="{% if item.active != true %} disabled {% endif %}" style="{% if item.sequence == 0 %} display: none; {% endif %}">
+                                        <a href="javascript:;" onclick="disable_at(this);">
+                                            禁用
+                                        </a>
+                                    </li>
+                                    <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                    <li>
+                                        <a href="javascript:;" data-toggle="modal" data-target="#update_os_template_image_profile_id_modal"
+                                           onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
+                                        $('#update_os_template_image_profile_id_instance_desc').text($(this).parent().parent().parent().parent().parent().children()[2].textContent)">
+                                            变更发行版本
+                                        </a>
+                                    </li>
+                                    <li class="divider" style="{% if item.sequence == 0 %} display: none; {% endif %}"></li>
+                                    <li>
+                                        <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                           onclick="$('#os_template_image_id').val($(this).parent().parent().parent().parent().parent().children()[0].textContent);
+                                       $('#delete_instance_desc').text($(this).parent().parent().parent().parent().parent().children()[2].textContent)">
+                                            删除
+                                        </a>
+                                    </li>
+                                </ul>
+                            </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 %}
+            </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>
+                                        {% 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>
-                        </th>
-                    </tr>
-                    </tfoot>
-                </table>
-            </div>
+                        </div>
+                    </th>
+                </tr>
+                </tfoot>
+            </table>
         </div>
     </div>
 </div>

+ 138 - 0
templates/snapshots_show.html

@@ -259,6 +259,89 @@
         highlight_selected_element();
         shortcut_bar_enable();
     }
+    
+    function make_list_of_disks(me) {
+        var snapshot_id = $('#snapshot_id').val();
+        var disks_uuid = [];
+
+        $.ajax({
+            url : '/api/snapshot/_disks/' + snapshot_id,
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            async: false,
+            error : function() {
+                alter_danger('获取实例磁盘列表失败!');
+            },
+            success : function(data, textStatus, xhr) {
+                disks_uuid = data.data;
+            }
+        });
+
+        $.ajax({
+            url : '/api/disks?order_by=sequence&filter=uuid:in:' + disks_uuid.join(','),
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            async: false,
+            error : function() {
+                alter_danger('获取实例磁盘列表失败!');
+            },
+            success : function(data, textStatus, xhr) {
+                var disks_list_tbody = $("#disks_list").find("tbody");
+                var disk_kind = '系统盘';
+                disks_list_tbody.empty();
+
+                $.each(data.data, function(k, v) {
+                    if (v['sequence'] === 0) {
+                        disk_kind = '系统盘';
+                    } else {
+                        disk_kind = '数据盘';
+                    }
+                    disks_list_tbody.append(
+                        '<tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">' +
+                        '<td>' +
+                            '<div>' + v['uuid'] + '</div>' +
+                            '<div>' + '<p style="display: inline-block;">' + v['remark'] + '</p></div>' +
+                        '</td>' +
+                        '<td>' + v['device'] + '</td>' +
+                        '<td>' + v['size'] + ' GB</td>' +
+                        '<td>' + disk_kind + '</td>' +
+                        '<td><a href="javascript:;" style="color: #0066cc" data-toggle="modal" data-target="#create_os_template_image_from_disk_snapshot_modal" ' +
+                        'onclick="$(\'#os_template_image_label\').val($(this).parent().prev().prev().prev().prev().children()[1].textContent);$(\'#disk_uuid\').val(\'' + v['uuid'] + '\');' +
+                        '$(\'#create_os_template_image_from_disk_snapshot_desc\').text($(this).parent().prev().prev().prev().text());$(\'#snapshot_convert_to_os_template_image_modal\').modal(\'hide\');">创建</a></td>' +
+                        '</tr>'
+                        );
+                });
+            }
+        });
+    }
+    
+    function create_os_template_image_from_disk_snapshot() {
+        var snapshot_id = $('#snapshot_id').val();
+        var disk_uuid = $('#disk_uuid').val();
+        var label = $('#os_template_image_label').val();
+
+        $('#create_os_template_image_from_disk_snapshot_modal').modal('hide');
+
+        $.ajax({
+            url : '/api/snapshot/_convert_to_os_template_image/' + snapshot_id + '/' + disk_uuid,
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                label: label
+            }),
+            error : function() {
+                alter_danger('从磁盘快照创建自定义镜像失败!');
+            },
+            success : function() {
+                alter_success('从磁盘快照创建自定义镜像成功!');
+                setTimeout(function() {
+                    window.location.href="/os_templates_image";
+                }, 1000);
+            }
+        });
+    }
 </script>
 <div class="panel">
     <div class="panel-body">
@@ -332,6 +415,10 @@
                             <a href="javascript:;" class="{% if item.progress not in [100] %} a-disabled {% endif %}" style="color: #0066cc" data-toggle="modal" data-target="#snapshot_revert_modal" onclick="$('#snapshot_id').val($(this).parent().parent().parent().children()[0].textContent);
                                                                                                                                             $('#revert_snapshot_instance_desc').text($($(this).parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().children()[2]).find('div p')[0].textContent)">恢复</a>
                             <span> | </span>
+                            <a href="javascript:;" data-guest_uuid="{{ item.guest_uuid }}" class="{% if item.progress not in [100] %} a-disabled {% endif %}" style="color: #0066cc" data-toggle="modal" data-target="#snapshot_convert_to_os_template_image_modal" onclick="$('#snapshot_id').val($(this).parent().parent().parent().children()[0].textContent);
+                                                                                                                                            make_list_of_disks(this);
+                                                                                                                                            $('#convert_snapshot_instance_desc').text($($(this).parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().children()[2]).find('div p')[0].textContent)">创建镜像</a>
+                            <span> | </span>
                             <a href="javascript:;" class="{% if item.progress not in [100, 255] %} a-disabled {% endif %}" style="color: #0066cc" data-toggle="modal" data-target="#snapshot_delete_modal" onclick="$('#snapshot_id').val($(this).parent().parent().parent().children()[0].textContent);
                                                                                                                                             $('#delete_snapshot_instance_desc').text($($(this).parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().children()[2]).find('div p')[0].textContent)">删除</a>
                         </div>
@@ -385,6 +472,7 @@
 </div>
 
 <input id="snapshot_id" title="快照 ID" class="form-control" name="snapshot_id" hidden>
+<input id="disk_uuid" title="磁盘 UUID" class="form-control" name="disk_uuid" hidden>
 
 <div class="modal" id="edit_label_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
     <div class="modal-dialog modal-sm">
@@ -421,6 +509,56 @@
     </div>
 </div>
 
+<div class="modal" id="snapshot_convert_to_os_template_image_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog modal-lg">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">请选择自定义镜像的磁盘:</h4>
+            </div>
+            <div class="modal-body">
+                <h3 style="color: orangered;" id="convert_snapshot_instance_desc"></h3>
+                <p></p>
+                <table id="disks_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 width="280px;">UUID</th>
+                        <th>设备路径</th>
+                        <th>大小</th>
+                        <th>磁盘性别</th>
+                        <th>操作</th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="create_os_template_image_from_disk_snapshot_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">
+                <h3 style="color: orangered;" id="create_os_template_image_from_disk_snapshot_desc"></h3>
+                <p></p>
+                <input id="os_template_image_label" title="自定义镜像名称" class="form-control" name="os_template_image_label">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="create_os_template_image_from_disk_snapshot();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
 <div class="modal" id="snapshot_delete_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
     <div class="modal-dialog">
         <div class="modal-content">

+ 3 - 1
views/os_template_image.py

@@ -107,6 +107,7 @@ def create():
         logo = request.form.get('logo')
         active = request.form.get('active', 1)
         os_template_profile_id = request.form.get('os_template_profile_id')
+        kind = request.form.get('kind', 0)
 
         payload = {
             "label": label,
@@ -114,7 +115,8 @@ def create():
             "path": path,
             "logo": logo,
             "active": active,
-            "os_template_profile_id": int(os_template_profile_id)
+            "os_template_profile_id": int(os_template_profile_id),
+            "kind": int(kind)
         }
 
         url = host_url + '/api/os_template_image'