Parcourir la source

加入磁盘挂载、卸载功能

James Iter il y a 9 ans
Parent
commit
4b5559bf15
14 fichiers modifiés avec 790 ajouts et 66 suppressions
  1. 24 19
      api/disk.py
  2. 8 8
      api/guest.py
  3. 4 0
      main.py
  4. 6 3
      misc/init.sql
  5. 7 6
      models/guest.py
  6. 1 1
      models/guest_xml.py
  7. 5 1
      models/initialize.py
  8. 45 1
      models/utils.py
  9. 538 0
      templates/disk_show.html
  10. 18 24
      templates/guest_show.html
  11. 1 1
      templates/layout.html
  12. 128 0
      views/disk.py
  13. 2 1
      views/guest.py
  14. 3 1
      views_route_table.py

+ 24 - 19
api/disk.py

@@ -44,7 +44,9 @@ disk_base = Base(the_class=Disk, the_blueprint=blueprint, the_blueprints=bluepri
 def r_create():
 
     args_rules = [
-        Rules.DISK_SIZE.value
+        Rules.DISK_SIZE.value,
+        Rules.REMARK.value,
+        Rules.QUANTITY.value
     ]
 
     try:
@@ -54,31 +56,34 @@ def r_create():
         ret['state'] = ji.Common.exchange_state(20000)
 
         size = request.json['size']
+        quantity = request.json.get('quantity')
 
         if size < 1:
             ret['state'] = ji.Common.exchange_state(41255)
             return ret
 
-        disk = Disk()
-        disk.guest_uuid = ''
-        disk.size = size
-        disk.uuid = uuid4().__str__()
-        disk.label = ji.Common.generate_random_code(length=8)
-        disk.sequence = -1
-        disk.format = 'qcow2'
+        while quantity:
+            quantity -= 1
+            disk = Disk()
+            disk.guest_uuid = ''
+            disk.size = size
+            disk.uuid = uuid4().__str__()
+            disk.remark = request.json.get('remark', '')
+            disk.sequence = -1
+            disk.format = 'qcow2'
 
-        config = Config()
-        config.id = 1
-        config.get()
+            config = Config()
+            config.id = 1
+            config.get()
 
-        disk.path = config.storage_path + '/' + disk.uuid + '.' + disk.format
+            disk.path = config.storage_path + '/' + disk.uuid + '.' + disk.format
 
-        message = {'action': 'create_disk', 'glusterfs_volume': config.glusterfs_volume,
-                   'image_path': disk.path, 'size': disk.size, 'uuid': disk.uuid}
+            message = {'action': 'create_disk', 'glusterfs_volume': config.glusterfs_volume,
+                       'image_path': disk.path, 'size': disk.size, 'uuid': disk.uuid}
 
-        db.r.rpush(app.config['downstream_queue'], json.dumps(message, ensure_ascii=False))
+            db.r.rpush(app.config['downstream_queue'], json.dumps(message, ensure_ascii=False))
 
-        disk.create()
+            disk.create()
 
         return ret
 
@@ -200,9 +205,9 @@ def r_update(uuid):
         Rules.UUID.value
     ]
 
-    if 'label' in request.json:
+    if 'remark' in request.json:
         args_rules.append(
-            Rules.LABEL.value,
+            Rules.REMARK.value
         )
 
     if args_rules.__len__() < 2:
@@ -218,7 +223,7 @@ def r_update(uuid):
         disk.uuid = uuid
         disk.get_by('uuid')
 
-        disk.label = request.json.get('label', disk.label)
+        disk.remark = request.json.get('remark', disk.remark)
 
         disk.update()
         disk.get()

+ 8 - 8
api/guest.py

@@ -98,7 +98,7 @@ def r_create():
             # 虚拟机内存单位,模板生成方法中已置其为GiB
             guest.memory = request.json.get('memory')
             guest.os_template_id = request.json.get('os_template_id')
-            guest.name = ji.Common.generate_random_code(length=8)
+            guest.label = ji.Common.generate_random_code(length=8)
             guest.remark = request.json.get('remark', '')
 
             guest.password = request.json.get('password')
@@ -118,7 +118,7 @@ def r_create():
 
             disk = Disk()
             disk.uuid = guest.uuid
-            disk.label = guest.name + '_SystemImage'
+            disk.remark = guest.label + '_SystemImage'
             disk.format = 'qcow2'
             disk.sequence = 0
             disk.size = 0
@@ -134,7 +134,7 @@ def r_create():
             _boot_jobs = copy.deepcopy(boot_jobs)
             for k, v in enumerate(_boot_jobs):
                 _boot_jobs[k]['content'] = v['content'].replace('{IP}', guest.ip).\
-                    replace('{HOSTNAME}', guest.name). \
+                    replace('{HOSTNAME}', guest.label). \
                     replace('{PASSWORD}', guest.password). \
                     replace('{NETMASK}', config.netmask).\
                     replace('{GATEWAY}', config.gateway).\
@@ -142,7 +142,7 @@ def r_create():
                     replace('{DNS2}', config.dns2)
 
                 _boot_jobs[k]['command'] = v['command'].replace('{IP}', guest.ip). \
-                    replace('{HOSTNAME}', guest.name). \
+                    replace('{HOSTNAME}', guest.label). \
                     replace('{PASSWORD}', guest.password). \
                     replace('{NETMASK}', config.netmask). \
                     replace('{GATEWAY}', config.gateway). \
@@ -152,7 +152,7 @@ def r_create():
             create_vm_msg = {
                 'action': 'create_guest',
                 'uuid': guest.uuid,
-                'name': guest.name,
+                'name': guest.label,
                 'glusterfs_volume': config.glusterfs_volume,
                 'template_path': os_template.path,
                 'disk': disk.__dict__,
@@ -307,7 +307,7 @@ def r_boot(uuids):
             # 替换占位符为有效内容
             for k, v in enumerate(boot_jobs):
                 boot_jobs[k]['content'] = v['content'].replace('{IP}', guest.ip). \
-                    replace('{HOSTNAME}', guest.name). \
+                    replace('{HOSTNAME}', guest.label). \
                     replace('{PASSWORD}', guest.password). \
                     replace('{NETMASK}', config.netmask). \
                     replace('{GATEWAY}', config.gateway). \
@@ -315,7 +315,7 @@ def r_boot(uuids):
                     replace('{DNS2}', config.dns2)
 
                 boot_jobs[k]['command'] = v['command'].replace('{IP}', guest.ip). \
-                    replace('{HOSTNAME}', guest.name). \
+                    replace('{HOSTNAME}', guest.label). \
                     replace('{PASSWORD}', guest.password). \
                     replace('{NETMASK}', config.netmask). \
                     replace('{GATEWAY}', config.gateway). \
@@ -605,7 +605,7 @@ def r_update(uuid):
         guest.uuid = uuid
         guest.get_by('uuid')
 
-        guest.remark = request.json.get('remark', guest.name)
+        guest.remark = request.json.get('remark', guest.label)
 
         guest.update()
         guest.get()

+ 4 - 0
main.py

@@ -38,6 +38,8 @@ from api.host import blueprints as host_blueprints
 
 from views.guest import blueprint as view_guest_blueprint
 from views.guest import blueprints as view_guest_blueprints
+from views.disk import blueprint as view_disk_blueprint
+from views.disk import blueprints as view_disk_blueprints
 
 from websockify.websocketproxy import WebSocketProxy
 
@@ -105,6 +107,8 @@ try:
 
     app.register_blueprint(view_guest_blueprint)
     app.register_blueprint(view_guest_blueprints)
+    app.register_blueprint(view_disk_blueprint)
+    app.register_blueprint(view_disk_blueprints)
 
 except:
     logger.error(traceback.format_exc())

+ 6 - 3
misc/init.sql

@@ -6,7 +6,7 @@ USE jimv;
 CREATE TABLE IF NOT EXISTS guest(
     id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     uuid CHAR(36) NOT NULL,
-    name VARCHAR(64) NOT NULL UNIQUE,
+    label VARCHAR(64) NOT NULL UNIQUE,
     password VARCHAR(255) NOT NULL,
     remark VARCHAR(255) NOT NULL DEFAULT '',
     os_template_id BIGINT UNSIGNED NOT NULL,
@@ -27,9 +27,10 @@ CREATE TABLE IF NOT EXISTS guest(
     DEFAULT CHARSET=utf8;
 
 ALTER TABLE guest ADD INDEX (uuid);
-ALTER TABLE guest ADD INDEX (name);
+ALTER TABLE guest ADD INDEX (label);
 ALTER TABLE guest ADD INDEX (on_host);
 ALTER TABLE guest ADD INDEX (ip);
+ALTER TABLE guest ADD INDEX (remark);
 
 
 CREATE TABLE IF NOT EXISTS guest_migrate_info(
@@ -57,13 +58,14 @@ ALTER TABLE guest_migrate_info ADD INDEX (uuid);
 CREATE TABLE IF NOT EXISTS disk(
     id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
     uuid CHAR(36) NOT NULL,
-    label VARCHAR(255) NOT NULL,
     path VARCHAR(255) NOT NULL,
     size INT UNSIGNED NOT NULL,
+    remark VARCHAR(255) NOT NULL DEFAULT '',
     sequence TINYINT NOT NULL,
     format CHAR(16) NOT NULL DEFAULT 'qcow2',
     -- 实例固有的状态用 state;
     state TINYINT UNSIGNED NOT NULL DEFAULT 0,
+    create_time BIGINT UNSIGNED NOT NULL,
     guest_uuid CHAR(36) NOT NULL,
     PRIMARY KEY (id))
     ENGINE=InnoDB
@@ -71,6 +73,7 @@ CREATE TABLE IF NOT EXISTS disk(
 
 ALTER TABLE disk ADD INDEX (size);
 ALTER TABLE disk ADD INDEX (guest_uuid);
+ALTER TABLE disk ADD INDEX (remark);
 
 
 CREATE TABLE IF NOT EXISTS os_template(

+ 7 - 6
models/guest.py

@@ -26,7 +26,7 @@ class Guest(ORM):
         super(Guest, self).__init__()
         self.id = 0
         self.uuid = None
-        self.name = None
+        self.label = None
         self.password = None
         self.remark = ''
         self.os_template_id = None
@@ -47,7 +47,7 @@ class Guest(ORM):
         return {
             'id': FilterFieldType.INT.value,
             'uuid': FilterFieldType.STR.value,
-            'name': FilterFieldType.STR.value,
+            'label': FilterFieldType.STR.value,
             'remark': FilterFieldType.STR.value,
             'on_host': FilterFieldType.STR.value,
             'ip': FilterFieldType.STR.value
@@ -59,7 +59,7 @@ class Guest(ORM):
 
     @staticmethod
     def get_allow_content_search_keywords():
-        return ['name', 'remark', 'on_host', 'ip']
+        return ['label', 'remark', 'on_host', 'ip']
 
     @staticmethod
     def emit_instruction(message):
@@ -100,12 +100,13 @@ class Disk(ORM):
         super(Disk, self).__init__()
         self.id = 0
         self.uuid = None
-        self.label = None
+        self.remark = None
         self.path = None
         self.size = None
         self.sequence = None
         self.state = DiskState.pending.value
         self.format = 'qcow2'
+        self.create_time = ji.Common.tus()
         self.guest_uuid = None
 
     @staticmethod
@@ -113,7 +114,7 @@ class Disk(ORM):
         return {
             'id': FilterFieldType.INT.value,
             'uuid': FilterFieldType.STR.value,
-            'label': FilterFieldType.STR.value,
+            'remark': FilterFieldType.STR.value,
             'size': FilterFieldType.INT.value,
             'state': FilterFieldType.INT.value,
             'guest_uuid': FilterFieldType.STR.value
@@ -125,7 +126,7 @@ class Disk(ORM):
 
     @staticmethod
     def get_allow_content_search_keywords():
-        return ['label', 'size']
+        return ['remark', 'size']
 
 
 class GuestMigrateInfo(ORM):

+ 1 - 1
models/guest_xml.py

@@ -86,7 +86,7 @@ class GuestXML(object):
         """
 
     def get_name(self):
-        return """<name>{0}</name>""".format(self.guest.name)
+        return """<name>{0}</name>""".format(self.guest.label)
 
     def get_uuid(self):
         return """<uuid>{0}</uuid>""".format(self.guest.uuid)

+ 5 - 1
models/initialize.py

@@ -106,4 +106,8 @@ app.config = dict(app.config, **config)
 ji.index_state['branch'] = dict(ji.index_state['branch'], **own_state_branch)
 
 # sequence_device_node_mapping = ['vda', 'vdb', 'vdc', 'vdd']
-dev_table = ['vda', 'vdb', 'vdc', 'vdd']
+dev_table = list()
+
+for i in range(26):
+    dev_table.append('vd' + chr(97 + i))
+

+ 45 - 1
models/utils.py

@@ -206,6 +206,50 @@ def utility_processor():
         return '<span class="{icon}" style="color: #{color};">&nbsp;&nbsp;{desc}</span>'.format(
             icon=icon, color=color, desc=desc)
 
+    def format_sequence_to_device_name(sequence):
+        # sequence 不能大于 25。dev_table 序数从 0 开始。
+        if sequence == -1:
+            return u'无'
+
+        if sequence >= dev_table.__len__():
+            return 'Unknown'
+
+        return dev_table[sequence]
+
+    def format_disk_state(state):
+        from status import DiskState
+
+        color = 'FF645B'
+        icon = 'glyph-icon icon-bolt'
+        desc = '未知状态'
+
+        if state == DiskState.pending.value:
+            color = 'FFC543'
+            icon = 'glyph-icon icon-spinner'
+            desc = '创建中'
+
+        elif state == DiskState.idle.value:
+            color = '0077BB'
+            icon = 'glyph-icon icon-unlink'
+            desc = '待挂载'
+
+        elif state == DiskState.mounted.value:
+            color = '00BB00'
+            icon = 'glyph-icon icon-link'
+            desc = '使用中'
+
+        elif state == DiskState.dirty.value:
+            color = 'FF0707'
+            icon = 'glyph-icon icon-remove'
+            desc = '创建失败,待清理'
+
+        else:
+            pass
+
+        return '<span class="{icon}" style="color: #{color};">&nbsp;&nbsp;{desc}</span>'.format(
+            icon=icon, color=color, desc=desc)
+
     return dict(format_price=format_price, format_datetime_by_tus=format_datetime_by_tus,
-                format_guest_status=format_guest_status)
+                format_guest_status=format_guest_status, format_sequence_to_device_name=format_sequence_to_device_name,
+                format_disk_state=format_disk_state)
 

+ 538 - 0
templates/disk_show.html

@@ -0,0 +1,538 @@
+{% extends "layout.html" %}
+{% block title %} Disk {% endblock %}
+{% block head %}
+    {{ super() }}
+    <style type="text/css">
+        .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;
+            height: 106px;
+        }
+
+        .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: #fffdf4 !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();
+        });
+
+        $('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);
+        });
+
+        $('#edit_remark_modal').on('show.bs.modal', function (me) {
+            $('#instance_uuid').val($(me.relatedTarget).parent().parent().find('div a')[0].text);
+            $('#edit_remark').val($(me.relatedTarget).prev().text());
+        });
+
+        $('#mountable_guest').chosen({
+            "search_contains": true
+        });
+
+        $('#mount_modal').on('show.bs.modal', function (me) {
+            $.ajax({
+                url : '/api/guests?page=1&page_size=10000',
+                type : 'GET',
+                contentType: "application/json; charset=utf-8",
+                dataType: 'json',
+                error : function() {
+                },
+                success : function(data, textStatus, xhr) {
+                    $('#mountable_guest').empty();
+                    $('#mountable_guest').append(
+                        $('<option>')
+                    );
+                    $.each(data.data, function(k, v) {
+                        $('#mountable_guest').append(
+                            $('<option>', {value: v['uuid'], text: v['label'] + '/' + v['remark']})
+                        );
+                    });
+                    $('#mountable_guest').trigger("chosen:updated");
+                }
+            });
+        });
+    });
+
+    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_remark_trigger").css('display','inline-flex');
+    }
+
+    function row_onmouseout(me) {
+        $(me).find(".edit_remark_trigger").css('display','none');
+    }
+
+    function remark_update(me) {
+        var uuid = $('#instance_uuid').val();
+        var remark = $('#edit_remark').val();
+        $('#edit_remark_modal').modal('hide');
+        $.ajax({
+            url : '/api/disk/' + uuid,
+            type : 'PATCH',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                remark: remark
+            }),
+            error : function() {
+                alter_danger('磁盘备注更新失败!');
+            },
+            success : function() {
+                $('#disk_list tbody').find('td:contains(' + uuid + ')').find('p').text(remark);
+                alter_success('磁盘备注更新成功!');
+            }
+        });
+    }
+
+    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();
+    }
+
+    function mount(uuid, disk_uuid) {
+        $.ajax({
+            url : '/api/guest/_attach_disk/' + uuid + '/' + disk_uuid,
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('实例挂载指令发送失败!');
+            },
+            success : function() {
+                alter_success('实例挂载指令发送成功!');
+            }
+        });
+    }
+
+    function unmount(disk_uuid) {
+        $.ajax({
+            url : '/api/guest/_detach_disk/' + disk_uuid,
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('实例卸载指令发送失败!');
+            },
+            success : function() {
+                alter_success('实例卸载指令发送成功!');
+            }
+        });
+    }
+
+    function resize(disk_uuid) {
+        $.ajax({
+            url : '/api/disk/_disk_resize/' + disk_uuid,
+            type : 'PUT',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('实例扩容指令发送失败!');
+            },
+            success : function() {
+                alter_success('实例扩容指令发送成功!');
+            }
+        });
+    }
+
+    function remove(uuids) {
+        $.ajax({
+            url : '/api/disks/' + uuids.join(','),
+            type : 'DELETE',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('实例删除指令发送失败!');
+            },
+            success : function() {
+                alter_success('实例删除指令发送成功!');
+            }
+        });
+    }
+
+    function mount_at(me) {
+        var guest_uuid = $('#mountable_guest').val();
+        var disk_uuid = $('#instance_uuid').val();
+        mount(guest_uuid, disk_uuid);
+        $('#mount_modal').modal('hide');
+    }
+
+    function unmount_at(me) {
+        var disk_uuid = $($(me).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text;
+        unmount(disk_uuid);
+    }
+
+    function get_selected_uuids() {
+        var selected_element = get_selected_element(true);
+        var uuids = [];
+
+        selected_element.each(function(i, e) {
+            var uuid = $(e).parent().prev().text();
+            uuids.push(uuid);
+        });
+
+        return uuids;
+    }
+
+    function get_checked_uuids() {
+        var uuids = get_selected_uuids();
+
+        if (uuids.length < 1) {
+            alter_warning('未选择任何可操作的实例!');
+            return false;
+        }
+
+        return uuids;
+    }
+
+   function batch_delete() {
+        var uuids = get_checked_uuids();
+        if (uuids) {
+            remove(uuids);
+        }
+    }
+</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 add-page-transition" href="/disks/create" data-transition="pt-page-moveFromRight-init" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">创建磁盘</a>
+                        </div>
+                    </div>
+                </div>
+                <table id="disk_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><input class="all_selector" title="选取所有" type="checkbox"></th>
+                    <th width="280px;">UUID</th>
+                    <th>状态</th>
+                    <th>大小</th>
+                    <th>设备号</th>
+                    <th>所属 Guest</th>
+                    <th>创建时间</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                {% for item in disks_ret.data %}
+                <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                    <td><input title="选中" type="checkbox"></td>
+                    <td>
+                        <div>
+                            <a href="javascript:;">{{ item.uuid }}</a>
+                        </div>
+                        <div>
+                            <p style="display: inline-block;">{{ item.remark }}</p>
+                            <a href="javascript:;" class="edit_remark_trigger" data-toggle="modal" data-target="#edit_remark_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>{{ format_disk_state(item.state)|safe }}</td>
+                    <td>{{ item.size }} GB</td>
+                    <td>{{ format_sequence_to_device_name(item.sequence) }}</td>
+                    <td><a href="javascript:;">{{ item.guest_uuid }}</a></td>
+                    <td>{{ format_datetime_by_tus(item.create_time) }}</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.state not in [1] %} disabled {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#mount_modal"
+                                       onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[1]).find('div a')[0].text);">
+                                        挂载
+                                    </a>
+                                </li>
+                                <li class="{% if item.sequence == 0 or item.state not in [2] %} disabled {% endif %}">
+                                    <a href="javascript:;" onclick="unmount_at(this);">卸载</a>
+                                </li>
+                                <li class="divider"></li>
+                                <li class="{% if item.state not in [1, 2] %} disabled {% endif %}">
+                                    <a href="javascript:;" data-toggle="modal" data-target="#resize_modal"
+                                       onclick="$('#resize_type').val('single')">
+                                        磁盘扩容
+                                    </a>
+                                </li>
+                                <li class="divider"></li>
+                                <li class="{% if item.state not in [1, 255] %} disabled {% endif %}">
+                                    <a href="javascript:;" onclick="delete_at(this);">删除</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">
+                                    <button class="btn btn-default btn-shortcut disabled" onclick="batch_boot();">挂载</button>
+                                    <button class="btn btn-default btn-shortcut disabled" onclick="batch_reboot();">卸载</button>
+                                    <button class="btn btn-default btn-shortcut disabled" onclick="batch_shutdown();">磁盘扩容</button>
+                                    <button class="btn btn-default btn-shortcut disabled" onclick="batch_suspend();">删除</button>
+                                </div>
+                                <div class="col-sm-3" style="font-size: 12px; padding-top: 5px; text-align: right;">
+                                    共有{{ disks_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 %}">«</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 %}">{{ 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 %}">»</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </th>
+                    </tr>
+                    </tfoot>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<input id="instance_uuid" title="实例 UUID" class="form-control" name="uuid" hidden>
+
+<div class="modal" id="edit_remark_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_remark" title="实例名称" class="form-control" name="remark">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="remark_update();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="mount_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">
+                <select id="mountable_guest" name="mountable_host" title="可被挂载的 Guest" class="chosen-select" data-placeholder="挂载至 ...">
+                    <option></option>
+                </select>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="mount_at();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="reset_password_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">
+                <div class="form-group">
+                    <input id="reset_password_type" title="重置密码类型" class="form-control" name="reset_password_type" value="single" hidden>
+                    <input id="reset_password_instance_uuid" title="实例 UUID" class="form-control" name="uuid" hidden>
+                    <input id="new_password" title="新密码" class="form-control" name="new_password" type="password" placeholder="New password">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="reset_password_go();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock content %}

+ 18 - 24
templates/guest_show.html

@@ -65,11 +65,12 @@
     var page = 1;
     var page_size = 10;
     var keyword = '';
-    var cur_url = '/guests';
+    var resource_path = window.location.pathname;
+    var cur_url = resource_path;
 
     $(document).ready(function() {
         page_size = $('#page_size').val();
-        cur_url = '/guests?page=' + page + '&page_size=' + page_size;
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
 
         var last_ready = null;
         $('#content_search').keydown(function() {
@@ -78,9 +79,9 @@
             }
             last_ready = setTimeout(function () {
                 keyword = $('#content_search').val();
-                cur_url = '/guests?page=' + page + '&page_size=' + page_size;
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
                 if (keyword.length > 0) {
-                    cur_url = '/guests?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+                    cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
                 }
                 window.location.href=cur_url;
             }, 1000);
@@ -89,9 +90,9 @@
         $('#page_size').change(function () {
             keyword = $('#content_search').val();
             page_size = $('#page_size').val();
-            cur_url = '/guests?page=' + page + '&page_size=' + page_size;
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
             if (keyword.length > 0) {
-                cur_url = '/guests?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
             }
             window.location.href=cur_url;
         });
@@ -172,9 +173,9 @@
         keyword = $('#content_search').val();
         page = $('#pagination li.active a').text();
         page_size = $('#page_size').val();
-        cur_url = '/guests?page=' + page + '&page_size=' + page_size;
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
         if (keyword.length > 0) {
-            cur_url = '/guests?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
         }
         window.location.href=cur_url;
     }
@@ -576,7 +577,7 @@
                     <td><input title="选中" type="checkbox"></td>
                     <td>
                         <div>
-                            <a href="javascript:;">{{ item.name }}</a>
+                            <a href="javascript:;">{{ item.label }}</a>
                         </div>
                         <div>
                             <p style="display: inline-block;">{{ item.remark }}</p>
@@ -695,8 +696,6 @@
                                 <div class="col-sm-3" style="font-size: 12px; padding-top: 5px; text-align: right;">
                                     共有{{ guests_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="1" {% if page_size == 1 %} selected {% endif %}>1</option>
-                                        <option value="2" {% if page_size == 2 %} selected {% endif %}>2</option>
                                         <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>
@@ -706,15 +705,15 @@
                                     <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="/guests?page={{ page - 1 }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">«</a>
+                                                <a href="{{ resource_path }}?page={{ page - 1 }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">«</a>
                                             </li>
                                             {% for item in pages %}
                                             <li class="{% if item == page %} active {% endif %}">
-                                                <a href="/guests?page={{ item }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">{{ item }}</a>
+                                                <a href="{{ resource_path }}?page={{ item }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">{{ item }}</a>
                                             </li>
                                             {% endfor %}
                                             <li class="{% if page == last_page %} disabled {% endif %}">
-                                                <a href="/guests?page={{ page + 1 }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">»</a>
+                                                <a href="{{ resource_path }}?page={{ page + 1 }}&page_size={{ page_size }}{% if keyword %} &keyword={{ keyword }} {% endif %}">»</a>
                                             </li>
                                         </ul>
                                     </div>
@@ -774,16 +773,11 @@
                 <h4 class="modal-title">重置密码:</h4>
             </div>
             <div class="modal-body">
-                <form class="form-horizontal">
-                    <div class="form-group">
-                        <input id="reset_password_type" title="重置密码类型" class="form-control" name="reset_password_type" value="single" hidden>
-                        <input id="reset_password_instance_uuid" title="实例 UUID" class="form-control" name="uuid" hidden>
-                        <label class="col-sm-3 control-label" for="new_password">新密码:</label>
-                        <div class="col-sm-9">
-                            <input id="new_password" title="新密码" class="form-control" name="new_password" type="password" placeholder="New password">
-                        </div>
-                    </div>
-                </form>
+                <div class="form-group">
+                    <input id="reset_password_type" title="重置密码类型" class="form-control" name="reset_password_type" value="single" hidden>
+                    <input id="reset_password_instance_uuid" title="实例 UUID" class="form-control" name="uuid" hidden>
+                    <input id="new_password" title="新密码" class="form-control" name="new_password" type="password" placeholder="New password">
+                </div>
             </div>
             <div class="modal-footer">
                 <button type="button" class="btn btn-sm btn-primary" onclick="reset_password_go();">确定</button>

+ 1 - 1
templates/layout.html

@@ -300,7 +300,7 @@
                             </a>
                         </li>
                         <li>
-                            <a href="{{ url_for('v_guests.show') }}" title="Disks">
+                            <a href="{{ url_for('v_disks.show') }}" title="Disks">
                                 <i class="glyph-icon icon-hdd-o"></i>
                                 <span>磁盘</span>
                             </a>

+ 128 - 0
views/disk.py

@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+from flask import Blueprint, render_template, url_for, request
+import requests
+from math import ceil
+import re
+
+
+__author__ = 'James Iter'
+__date__ = '2017/6/25'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2017 by James Iter.'
+
+
+blueprint = Blueprint(
+    'v_disk',
+    __name__,
+    url_prefix='/disk'
+)
+
+blueprints = Blueprint(
+    'v_disks',
+    __name__,
+    url_prefix='/disks'
+)
+
+
+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)
+    resource_path = request.path
+
+    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__())
+
+    host_url = request.host_url.rstrip('/')
+
+    disks_url = host_url + url_for('api_disks.r_get_by_filter')
+    if keyword is not None:
+        disks_url = host_url + url_for('api_disks.r_content_search')
+
+    if args.__len__() > 0:
+        disks_url = disks_url + '?' + '&'.join(args)
+
+    disks_ret = requests.get(url=disks_url)
+    disks_ret = json.loads(disks_ret.content)
+
+    last_page = int(ceil(disks_ret['paging']['total'] / 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:
+                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:
+                break
+
+    return render_template('disk_show.html', disks_ret=disks_ret, resource_path=resource_path, page=page,
+                           page_size=page_size, keyword=keyword, pages=pages, last_page=last_page)
+
+
+def create():
+    host_url = request.host_url.rstrip('/')
+
+    if request.method == 'POST':
+        ability = request.form.get('ability')
+        os_template_id = request.form.get('os_template_id')
+        quantity = request.form.get('quantity')
+        password = request.form.get('password')
+        remark = request.form.get('remark')
+
+        if not isinstance(ability, basestring):
+            pass
+
+        m = re.search('^(\d)c(\d)g$', ability.lower())
+        if m is None:
+            pass
+
+        cpu = m.groups()[0]
+        memory = m.groups()[1]
+
+        payload = {
+            "cpu": int(cpu),
+            "memory": int(memory),
+            "os_template_id": int(os_template_id),
+            "quantity": int(quantity),
+            "remark": remark,
+            "password": password,
+            "lease_term": 100
+        }
+
+        url = host_url + '/api/guest'
+        headers = {'content-type': 'application/json'}
+        r = requests.post(url, data=json.dumps(payload), headers=headers)
+        j_r = json.loads(r.content)
+        return render_template('success.html', go_back_url='/disks', timeout=10000, title='提交成功',
+                               message_title='创建实例的请求已被接受',
+                               message='您所提交的资源正在创建中。根据所提交资源的大小,需要等待几到十几分钟。页面将在10秒钟后自动跳转到实例列表页面!')
+
+    else:
+        os_template_url = host_url + url_for('api_os_templates.r_get_by_filter')
+        os_template_ret = requests.get(url=os_template_url)
+        os_template_ret = json.loads(os_template_ret.content)
+        return render_template('guest_create.html', os_template_data=os_template_ret['data'])
+
+

+ 2 - 1
views/guest.py

@@ -36,6 +36,7 @@ def show():
     page = int(request.args.get('page', 1))
     page_size = int(request.args.get('page_size', 10))
     keyword = request.args.get('keyword', None)
+    resource_path = request.path
 
     if page is not None:
         args.append('page=' + page.__str__())
@@ -87,7 +88,7 @@ def show():
             if i == last_page:
                 break
 
-    return render_template('guest_show.html', guests_ret=guests_ret,
+    return render_template('guest_show.html', guests_ret=guests_ret, resource_path=resource_path,
                            os_template_mapping_by_id=os_template_mapping_by_id, page=page,
                            page_size=page_size, keyword=keyword, pages=pages, last_page=last_page)
 

+ 3 - 1
views_route_table.py

@@ -3,7 +3,7 @@
 
 
 from models.utils import add_rule_views
-from views import guest
+from views import guest, disk
 
 
 __author__ = 'James Iter'
@@ -17,3 +17,5 @@ add_rule_views(guest.blueprints, '/create', views_func='guest.create', methods=[
 add_rule_views(guest.blueprints, '/success', views_func='guest.success', methods=['GET'])
 add_rule_views(guest.blueprint, '/vnc/<uuid>', views_func='guest.vnc', methods=['GET'])
 
+add_rule_views(disk.blueprints, '', views_func='disk.show', methods=['GET'])
+