Ver código fonte

前端实现 SSH Key CURD

James Iter 8 anos atrás
pai
commit
12eff6b5a8

+ 1 - 0
README.md

@@ -57,6 +57,7 @@
 |RESTful 风格的 API|✓|
 |Virtio设备|✓|
 |Guest 暂停/恢复|✓|
+|Guest 在线重置密码|✓|
 
 
 ## 未来计划

+ 2 - 0
api/guest.py

@@ -141,6 +141,8 @@ def r_create():
             db.r.sadd(app.config['vnc_port_used_set'], guest.vnc_port)
 
             guest.vnc_password = ji.Common.generate_random_code(length=16)
+            guest.ssh_keys_id = []
+            guest.ssh_keys_id = json.dumps(guest.ssh_keys_id)
 
             disk = Disk()
             disk.uuid = guest.uuid

+ 132 - 0
api/ssh_key.py

@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from flask import Blueprint
+from flask import request
+import json
+import jimit as ji
+
+from api.base import Base
+from models import SSHKey
+from models import Utils
+from models import Rules
+
+
+__author__ = 'James Iter'
+__date__ = '2018/2/26'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_ssh_key',
+    __name__,
+    url_prefix='/api/ssh_key'
+)
+
+blueprints = Blueprint(
+    'api_ssh_keys',
+    __name__,
+    url_prefix='/api/ssh_keys'
+)
+
+
+ssh_key_base = Base(the_class=SSHKey, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.LABEL.value,
+        Rules.PUBLIC_KEY.value
+    ]
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        ssh_key = SSHKey()
+
+        ssh_key.label = request.json.get('label')
+        ssh_key.public_key = request.json.get('public_key')
+
+        if ssh_key.exist_by('public_key'):
+            ret['state'] = ji.Common.exchange_state(40901)
+            ret['state']['sub']['zh-cn'] = ''.join([ret['state']['sub']['zh-cn'], ': ', ssh_key.public_key])
+            return ret
+
+        ssh_key.create()
+
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(_id):
+
+    ssh_key = SSHKey()
+
+    args_rules = [
+        Rules.ID.value
+    ]
+
+    if 'label' in request.json:
+        args_rules.append(
+            Rules.LABEL.value,
+        )
+
+    if 'public_key' in request.json:
+        args_rules.append(
+            Rules.PUBLIC_KEY.value,
+        )
+
+    if args_rules.__len__() < 2:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    request.json['id'] = _id
+
+    try:
+        ji.Check.previewing(args_rules, request.json)
+        ssh_key.id = request.json.get('id')
+
+        ssh_key.get()
+        ssh_key.label = request.json.get('label', ssh_key.label)
+        ssh_key.public_key = request.json.get('public_key', ssh_key.public_key)
+
+        ssh_key.update()
+        ssh_key.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = ssh_key.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(ids):
+    return ssh_key_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return ssh_key_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return ssh_key_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(ids):
+    return ssh_key_base.delete(ids=ids, ids_rule=Rules.IDS.value, by_field='id')

+ 10 - 0
api_route_table.py

@@ -15,6 +15,7 @@ from api import log
 from api import host
 from api import guest_performance
 from api import host_performance
+from api import ssh_key
 
 
 __author__ = 'James Iter'
@@ -133,6 +134,15 @@ add_rule_api(host.blueprints, '/_search', api_func='host.r_content_search', meth
 add_rule_api(host.blueprints, '/<nodes_id>', api_func='host.r_delete', methods=['DELETE'])
 add_rule_api(host.blueprints, '/<hosts_name>/<random>', api_func='host.r_nonrandom', methods=['PUT'])
 
+# SSH Key 操作
+add_rule_api(ssh_key.blueprint, '', api_func='ssh_key.r_create', methods=['POST'])
+add_rule_api(ssh_key.blueprints, '/<ids>', api_func='ssh_key.r_delete', methods=['DELETE'])
+add_rule_api(ssh_key.blueprints, '/<_id>', api_func='ssh_key.r_update', methods=['PATCH'])
+add_rule_api(ssh_key.blueprints, '/<ids>', api_func='ssh_key.r_get', methods=['GET'])
+add_rule_api(ssh_key.blueprints, '', api_func='ssh_key.r_get_by_filter', methods=['GET'])
+add_rule_api(ssh_key.blueprints, '/_search', api_func='ssh_key.r_content_search', methods=['GET'])
+
+# 日志查询
 # Guest 性能查询
 add_rule_api(guest_performance.blueprint, '/cpu_memory',
              api_func='guest_performance.r_cpu_memory_get_by_filter', methods=['GET'])

+ 1 - 0
docs/todo.md

@@ -78,5 +78,6 @@
 - [x] 取消单独的初始化密码操作,合并入具体的操作系统初始化操作中
 - [ ] 迁移中的虚拟机,不允许做任何操作
 - [ ] 通过 QemuGuestAgent 实现 Guest 的内存使用率监控
+- [ ] 在线重置密码,前端加入随机生成功能
 
 

+ 8 - 0
main.py

@@ -43,6 +43,8 @@ from api.log import blueprint as log_blueprint
 from api.log import blueprints as log_blueprints
 from api.host import blueprint as host_blueprint
 from api.host import blueprints as host_blueprints
+from api.ssh_key import blueprint as ssh_key_blueprint
+from api.ssh_key import blueprints as ssh_key_blueprints
 from api.guest_performance import blueprint as performance_blueprint
 from api.guest_performance import blueprints as performance_blueprints
 from api.host_performance import blueprint as host_performance_blueprint
@@ -60,6 +62,8 @@ from views.log import blueprint as view_log_blueprint
 from views.log import blueprints as view_log_blueprints
 from views.os_template_image import blueprint as view_os_template_image_blueprint
 from views.os_template_image import blueprints as view_os_template_image_blueprints
+from views.ssh_key import blueprint as view_ssh_key_blueprint
+from views.ssh_key import blueprints as view_ssh_key_blueprints
 
 from views.host import blueprint as view_host_blueprint
 from views.host import blueprints as view_host_blueprints
@@ -229,6 +233,8 @@ try:
     app.register_blueprint(log_blueprints)
     app.register_blueprint(host_blueprint)
     app.register_blueprint(host_blueprints)
+    app.register_blueprint(ssh_key_blueprint)
+    app.register_blueprint(ssh_key_blueprints)
     app.register_blueprint(performance_blueprint)
     app.register_blueprint(performance_blueprints)
     app.register_blueprint(host_performance_blueprint)
@@ -245,6 +251,8 @@ try:
     app.register_blueprint(view_log_blueprints)
     app.register_blueprint(view_os_template_image_blueprint)
     app.register_blueprint(view_os_template_image_blueprints)
+    app.register_blueprint(view_ssh_key_blueprint)
+    app.register_blueprint(view_ssh_key_blueprints)
 
     app.register_blueprint(view_host_blueprint)
     app.register_blueprint(view_host_blueprints)

+ 14 - 1
misc/init.sql

@@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS guest(
     manage_network VARCHAR(64) NOT NULL,
     vnc_port INT UNSIGNED NOT NULL,
     vnc_password VARCHAR(255) NOT NULL,
+    ssh_keys_id TEXT NOT NULL,
     xml TEXT NOT NULL,
     PRIMARY KEY (id))
     ENGINE=InnoDB
@@ -357,10 +358,22 @@ ALTER TABLE host_disk_usage_io ADD INDEX (timestamp);
 ALTER TABLE host_disk_usage_io ADD INDEX (node_id, mountpoint, timestamp);
 
 
+CREATE TABLE IF NOT EXISTS ssh_key(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    label VARCHAR(255) NOT NULL,
+    public_key TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE ssh_key ADD INDEX (label);
+
+
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-Systemd', '用作 Redhat Systemd 系列的系统初始化。初始化操作依据 CentOS 7 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('CentOS-SysV', '用作 Redhat SysV 系列的系统初始化。初始化操作依据 CentOS 6.8 来实现。', 1);
 INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Gentoo-OpenRC', '用作 Gentoo OpenRC 系列的系统初始化。', 1);
-INSERT INTO os_template_initialize_operate_set (label, descriptione, active) VALUES ('Windows', '用作 MS-Windows 系列的系统初始化。初始化操作依据 Windows 2012 来实现。', 1);
+INSERT INTO os_template_initialize_operate_set (label, description, active) VALUES ('Windows', '用作 MS-Windows 系列的系统初始化。初始化操作依据 Windows 2012 来实现。', 1);
 
 -- For CentOS-Systemd
 INSERT INTO os_template_initialize_operate (os_template_initialize_operate_set_id, kind, sequence, path, content, command) VALUES (1, 1, 0, '/etc/resolv.conf', 'nameserver {DNS1}

+ 15 - 0
misc/v0.2_to_v0.3/update.sql

@@ -1,5 +1,19 @@
 
 
+CREATE TABLE IF NOT EXISTS ssh_key(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    label VARCHAR(255) NOT NULL,
+    public_key TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE ssh_key ADD INDEX (label);
+
+ALTER TABLE guest ADD COLUMN ssh_keys_id TEXT AFTER xml;
+
+
 -- 操作系统模板镜像
 CREATE TABLE IF NOT EXISTS os_template_image(
     label VARCHAR(255) NOT NULL,
@@ -137,3 +151,4 @@ INSERT INTO os_template_profile (label, description, os_type, os_distro, os_majo
 VALUES ('Gentoo-2.2', 'Gentoo 2.2。', 'linux', 'gentoo', 2, 2, 'x86_64', 'Gentoo Base System release 2.2', 1, 'icon-os icon-os-gentoo', 3);
 INSERT INTO os_template_profile (label, description, os_type, os_distro, os_major, os_minor, os_arch, os_product_name, active, icon, os_template_initialize_operate_set_id)
 VALUES ('Windows-2012-R2-Standard', 'Windows 2012 R2 Standard。', 'windows', 'windows', 6, 3, 'x86_64', 'Windows Server 2012 R2 Standard', 1, 'icon-os icon-os-windows', 4);
+

+ 5 - 1
models/__init__.py

@@ -39,6 +39,10 @@ from guest import (
     Guest, Disk
 )
 
+from ssh_key import (
+    SSHKey
+)
+
 from os_template_image import (
     OSTemplateImage
 )
@@ -100,7 +104,7 @@ __copyright__ = '(c) 2017 by James Iter.'
 
 __all__ = [
     'Rules', 'Utils', 'Init', 'Database', 'FilterFieldType', 'Filter', 'EmitKind', 'GuestState', 'DiskState',
-    'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'GuestXML', 'Log',
+    'LogLevel', 'ORM', 'User', 'Config', 'Guest', 'Disk', 'GuestXML', 'Log', 'SSHKey',
     'OSTemplateImage', 'OSTemplateProfile', 'OSTemplateInitializeOperateSet', 'OSTemplateInitializeOperate',
     'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO', 'HostCPUMemory', 'HostTraffic',
     'HostDiskUsageIO', 'Host'

+ 2 - 1
models/guest.py

@@ -40,6 +40,7 @@ class Guest(ORM):
         self.vnc_port = None
         self.vnc_password = None
         self.xml = None
+        self.ssh_keys_id = None
 
     @staticmethod
     def get_filter_keywords():
@@ -55,7 +56,7 @@ class Guest(ORM):
 
     @staticmethod
     def get_allow_update_keywords():
-        return ['remark', 'cpu', 'memory', 'network', 'manage_network', 'vnc_password']
+        return ['remark', 'cpu', 'memory', 'network', 'manage_network', 'vnc_password', 'ssh_keys_id']
 
     @staticmethod
     def get_allow_content_search_keywords():

+ 2 - 2
models/rules.py

@@ -56,8 +56,8 @@ class Rules(Enum):
     BPS_CAP = (int, 'iops_cap')
     BPS_MAX = (int, 'iops_max')
     BPS_MAX_LENGTH = (int, 'iops_max_length')
-    RSA_PRIVATE = (basestring, 'rsa_private')
-    RSA_PUBLIC = (basestring, 'rsa_public')
+
+    PUBLIC_KEY = (basestring, 'public_key')
 
     UUID = (basestring, 'uuid', (36, 36))
     NODE_ID = (basestring, 'node_id', (16, 16))

+ 45 - 0
models/ssh_key.py

@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2018/2/26'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+class SSHKey(ORM):
+
+    _table_name = 'ssh_key'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(SSHKey, self).__init__()
+        self.id = 0
+        self.label = None
+        self.public_key = None
+        self.create_time = ji.Common.ts()
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'label': FilterFieldType.STR.value,
+            'public_key': FilterFieldType.STR.value,
+            'create_time': FilterFieldType.INT.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return []
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['label']
+

+ 6 - 0
templates/layout.html

@@ -367,6 +367,12 @@
                                 <span>虚拟机模板镜像</span>
                             </a>
                         </li>
+                        <li>
+                            <a href="{{ url_for('v_ssh_keys.show') }}" title="SSH Keys">
+                                <i class="glyph-icon icon-linecons-key"></i>
+                                <span>SSH Keys</span>
+                            </a>
+                        </li>
                         <li>
                             <a href="{{ url_for('v_config.show') }}" title="JimV 虚拟化平台系统配置">
                                 <i class="glyph-icon icon-linecons-cog"></i>

+ 522 - 0
templates/ssh_keys_show.html

@@ -0,0 +1,522 @@
+{% extends "layout.html" %}
+{% block head %}
+    {{ super() }}
+    <style type="text/css">
+
+        @media (min-width: 768px) {
+            .form-horizontal .control-label {
+                text-align: left;
+            }
+        }
+
+        label>span {
+            color: deepskyblue;
+        }
+
+        .table {
+            font-size: 12px;
+            border-width: 1px;
+            line-height: 20px;
+        }
+        .table > thead > tr > th,
+        .table > tfoot > tr > th {
+            color: #999999;
+            font-weight: normal;
+            border-bottom: 0 solid #e1e6eb;
+            background-color: #F5F6FA;
+        }
+        
+        .table > tbody > tr > td {
+            color: #424547;
+        }
+
+        .table-bordered > tbody > tr {
+            padding-top: 10px;
+            padding-bottom: 10px;
+        }
+
+        .table-bordered > thead > tr > th,
+        .table-bordered > tbody > tr > th,
+        .table-bordered > tfoot > tr > th,
+        .table-bordered > thead > tr > td,
+        .table-bordered > tbody > tr > td,
+        .table-bordered > tfoot > tr > td {
+            border-style: solid;
+            border-width: 1px 0 0 0;
+        }
+
+        .btn,
+        .form-control,
+        .modal-content {
+            border-radius: 0 !important;
+        }
+
+        .btn-shortcut {
+            font-size: 12px !important;
+            padding: 0 26px;
+        }
+
+        .show {
+            display: inline-block !important;
+        }
+
+        .tr-selected td,
+        .tr-selected {
+            color: #000 !important;
+            background: #fafaff !important;
+        }
+    </style>
+
+{% endblock head %}
+{% block content %}
+
+<script type="text/javascript">
+    var page = 1;
+    var page_size = 10;
+    var keyword = '';
+    var resource_path = window.location.pathname;
+    var cur_url = resource_path;
+
+    $(document).ready(function() {
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+
+        var last_ready = null;
+        $('#content_search').keydown(function() {
+            if (last_ready !== null) {
+                clearTimeout(last_ready);
+            }
+            last_ready = setTimeout(function () {
+                keyword = $('#content_search').val();
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+                if (keyword.length > 0) {
+                    cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+                }
+                window.location.href=cur_url;
+            }, 1000);
+        });
+
+        $('#page_size').change(function () {
+            keyword = $('#content_search').val();
+            page_size = $('#page_size').val();
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+            if (keyword.length > 0) {
+                cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+            }
+            window.location.href=cur_url;
+        });
+
+        $("thead").on('click', ".all_selector", function() {
+            if ($("thead .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tfoot").on('click', ".all_selector", function() {
+            if ($("tfoot .all_selector").is(':checked')) {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', true);
+                $(".all_selector").prop('checked', true);
+            } else {
+                $("tbody tr").find('td input[type="checkbox"]:eq(0)').prop('checked', false);
+                $(".all_selector").prop('checked', false);
+            }
+
+            select_item_action();
+        });
+
+        $("tbody tr").on('click', "input[type='checkbox']", function() {
+            select_item_action();
+        });
+
+        $('#edit_label_modal').on('show.bs.modal', function (me) {
+            $('#ssh_key_id').val($(me.relatedTarget).parent().parent().prev().prev().text());
+            $('#edit_label').val($(me.relatedTarget).prev().text());
+        });
+
+        $('#edit_public_key_modal').on('show.bs.modal', function (me) {
+            $('#ssh_key_id').val($(me.relatedTarget).parent().parent().prev().prev().prev().prev().text());
+            $('#edit_public_key').val($(me.relatedTarget).prev().text());
+        });
+
+        $('#add_ssh_key_modal').on('show.bs.modal', function (me) {
+            //
+        });
+
+        $('#add_ssh_key_form').formValidation({
+            framework: 'bootstrap4',
+            icon: {
+                valid: 'fa fa-check',
+                invalid: 'fa fa-times',
+                validating: 'fa fa-refresh'
+            },
+            // Since the Bootstrap Button hides the radio and checkbox
+            // We exclude the disabled elements only
+            excluded: ':disabled',
+            locale: 'zh_CN',
+            fields: {
+                label: {
+                    validators: {
+                        notEmpty: {},
+                        stringLength: {
+                            min: 2,
+                            max: 255
+                        }
+                    }
+                },
+                public_key: {
+                    validators: {
+                        notEmpty: {},
+                        stringLength: {
+                            min: 380,
+                            max: 420
+                        }
+                    }
+                }
+            }
+        })
+        .on('success.field.fv', function(e, data) {
+            if (data.fv.getInvalidFields().length > 0) {    // There is invalid field
+                data.fv.disableSubmitButtons(true);
+            }
+        })
+    });
+
+    function refresh() {
+        keyword = $('#content_search').val();
+        page = $('#pagination li.active a').text();
+        page_size = $('#page_size').val();
+        cur_url = resource_path + '?page=' + page + '&page_size=' + page_size;
+        if (keyword.length > 0) {
+            cur_url = resource_path + '?page=' + page + '&page_size=' + page_size + '&keyword=' + keyword;
+        }
+        window.location.href=cur_url;
+    }
+
+    function row_onmouseover(me) {
+        $(me).find(".edit_label_trigger, .edit_path_trigger").css('display','inline-flex');
+    }
+
+    function row_onmouseout(me) {
+        $(me).find(".edit_label_trigger, .edit_path_trigger").css('display','none');
+    }
+
+    function label_update(me) {
+        var ssh_key_id = $('#ssh_key_id').val();
+        var label = $('#edit_label').val();
+        $('#edit_label_modal').modal('hide');
+        $.ajax({
+            url : '/api/ssh_keys/' + ssh_key_id,
+            type : 'PATCH',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                label: label
+            }),
+            error : function() {
+                alter_danger('SSH Key 名称更新失败!');
+            },
+            success : function() {
+                alter_success('SSH Key 名称更新成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
+
+    function public_key_update(me) {
+        var ssh_key_id = $('#ssh_key_id').val();
+        var public_key = $('#edit_public_key').val();
+        $('#edit_public_key_modal').modal('hide');
+        $.ajax({
+            url : '/api/ssh_keys/' + ssh_key_id,
+            type : 'PATCH',
+            contentType: "application/json; charset=utf-8",
+            data : JSON.stringify({
+                public_key: public_key
+            }),
+            error : function() {
+                alter_danger('Public Key 更新失败!');
+            },
+            success : function() {
+                alter_success('Public Key 更新成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
+
+    function get_selected_element(checked) {
+        if (checked === null) {
+            checked = true;
+        }
+
+        if (checked) {
+            return $('tbody :checked');
+        } else {
+
+            return $('tbody input:not(:checked)');
+        }
+    }
+    
+    function highlight_selected_element() {
+        var selected_element = get_selected_element(true);
+        var no_selected_element = get_selected_element(false);
+
+        selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', true);
+        });
+
+        no_selected_element.each(function(i, e) {
+            $(e).parent().parent().toggleClass('tr-selected', false);
+        });
+    }
+    
+    function shortcut_bar_enable() {
+        var selected_element = get_selected_element(true);
+
+        if (selected_element.length > 0) {
+            $('.btn-shortcut').toggleClass('disabled', false);
+        } else {
+            $('.btn-shortcut').toggleClass('disabled', true);
+        }
+    }
+
+    function select_item_action() {
+        highlight_selected_element();
+        shortcut_bar_enable();
+    }
+
+    function remove(id) {
+        $.ajax({
+            url : '/api/ssh_keys/' + id,
+            type : 'DELETE',
+            contentType: "application/json; charset=utf-8",
+            error : function() {
+                alter_danger('SSH Key 删除指令发送失败!');
+            },
+            success : function() {
+                alter_success('SSH Key 删除指令发送成功!');
+                setTimeout(function() {
+                    refresh();
+                }, 1000);
+            }
+        });
+    }
+
+    function delete_at(me) {
+        var ssh_key_id = $('#ssh_key_id').val();
+        remove(ssh_key_id);
+        $('#delete_modal').modal('hide');
+    }
+</script>
+<div class="panel">
+    <div class="panel-body">
+        <h3 class="title-hero" style="font-size: 24px;">
+            SSH Keys
+        </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_ssh_key_modal" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">添加 SSH Key</a>
+                        </div>
+                    </div>
+                </div>
+                <table id="ssh_keys_list" class="table table-bordered table-hover" cellspacing="0" width="100%" role="grid"
+                       style="width: 100%; margin-bottom: 0; border-bottom-width: 0; table-layout: fixed;">
+                <thead>
+                <tr role="row">
+                    <th style="display: none;">ID</th>
+                    <th><input class="all_selector" title="选取所有" type="checkbox"></th>
+                    <th width="180px;">名称</th>
+                    <th width="480px;">Public Key</th>
+                    <th>创建时间</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                {% for item in ssh_keys_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 class="text-left" style="text-overflow: ellipsis; overflow: hidden;">
+                        {{ item.public_key }}
+                    </td>
+                    <td>
+                        {{ format_datetime_by_tus(item.create_time * 1000000) }}
+                    </td>
+                    <td>
+                        <div class="dropdown inline-block">
+                            <a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown">
+                                更多
+                            </a>
+                            <ul class="dropdown-menu">
+                                <li>
+                                    <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                       onclick="$('#ssh_key_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;">
+                                    共有{{ ssh_keys_ret.paging.total }}条,每页显示:
+                                    <select id="page_size" name="datatable-row-highlight_length" title="page_size" class="form-control" style="height: 22px; vertical-align: baseline;">
+                                        <option value="10" {% if page_size == 10 %} selected {% endif %}>10</option>
+                                        <option value="20" {% if page_size == 20 %} selected {% endif %}>20</option>
+                                        <option value="50" {% if page_size == 50  %} selected {% endif %}>50</option>
+                                    </select>&nbsp;&nbsp;条
+                                </div>
+                                <div class="col-sm-3" style="text-align: left;">
+                                    <div class="dataTables_paginate paging_bootstrap" id="datatable-row-highlight_paginate">
+                                        <ul id="pagination" class="pagination">
+                                            <li class="{% if page == 1 %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page - 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">«</a>
+                                            </li>
+                                            {% for item in pages %}
+                                            <li class="{% if item == page %} active {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ item }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">{{ item }}</a>
+                                            </li>
+                                            {% endfor %}
+                                            <li class="{% if page == last_page %} disabled {% endif %}">
+                                                <a href="{{ resource_path }}?page={{ page + 1 }}&page_size={{ page_size }}{% if keyword %}&keyword={{ keyword }}{% endif %}{% if order_by %}&order_by={{ order_by }}{% endif %}{% if order %}&order={{ order }}{% endif %}">»</a>
+                                            </li>
+                                        </ul>
+                                    </div>
+                                </div>
+                            </div>
+                        </th>
+                    </tr>
+                    </tfoot>
+                </table>
+            </div>
+        </div>
+    </div>
+</div>
+
+<input id="ssh_key_id" title="SSH Key ID" class="form-control" name="ssh_key_id" hidden>
+
+<div class="modal" id="edit_label_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">编辑 SSH Key 名称:</h4>
+            </div>
+            <div class="modal-body">
+                <input id="edit_label" title="SSH Key 名称" class="form-control" name="ssh_key_label">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="label_update();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="edit_public_key_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">编辑 SSH Key:</h4>
+            </div>
+            <div class="modal-body">
+                <input id="edit_public_key" title="Public Key" class="form-control" name="public_key">
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="public_key_update();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="delete_modal" tabindex="-1" role="dialog" style="margin-top: 100px;">
+    <div class="modal-dialog">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h4 class="modal-title">删除 SSH Key:</h4>
+            </div>
+            <div class="modal-body">
+                <p>你确定要删除该 SSH Key 吗?</p>
+                <h3 style="color: orangered;" id="delete_instance_desc"></h3>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-primary" onclick="delete_at();">确定</button>
+                <button type="button" class="btn btn-sm btn-default" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="modal" id="add_ssh_key_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">添加 SSH Key:</h4>
+            </div>
+            <div class="modal-body" style="padding-top: 0; padding-bottom: 0;">
+                <div class="example-box-wrapper">
+                    <form id="add_ssh_key_form" class="form-horizontal bordered-row" action="/ssh_key" method="post">
+                        <div class="form-group">
+                            <div class="col-sm-2"></div>
+                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-elusive-compass-circled"></span>&nbsp;&nbsp;名称</label>
+                            <div class="col-sm-6">
+                                <input id="label" name="label" type="text" title="SSH Key 名称" class="form-control">
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <div class="col-sm-2"></div>
+                            <label class="col-sm-2 control-label"><span class="glyph-icon icon-newspaper-o"></span>&nbsp;&nbsp;Public Key</label>
+                            <div class="col-sm-6">
+                                <textarea id="public_key" name="public_key" title="Public key" class="form-control"></textarea>
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <div class="col-sm-4"></div>
+                            <div class="col-sm-6 pull-right">
+                                <button type="submit" class="btn btn-blue-alt" style="width: 180px; height: 40px; font-size: 16px;" disabled>创建</button>
+                                <button class="btn btn-default" style="width: 64px; height: 40px; font-size: 16px;" data-dismiss="modal">取消</button>
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock content %}

+ 124 - 0
views/ssh_key.py

@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import json
+from flask import Blueprint, render_template, url_for, request, redirect
+import requests
+from math import ceil
+
+
+__author__ = 'James Iter'
+__date__ = '2018/2/27'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'v_ssh_key',
+    __name__,
+    url_prefix='/ssh_key'
+)
+
+blueprints = Blueprint(
+    'v_ssh_keys',
+    __name__,
+    url_prefix='/ssh_keys'
+)
+
+
+def show():
+    args = list()
+    page = int(request.args.get('page', 1))
+    page_size = int(request.args.get('page_size', 10))
+    keyword = request.args.get('keyword', None)
+    order_by = request.args.get('order_by', None)
+    order = request.args.get('order', None)
+    filters = list()
+
+    if page is not None:
+        args.append('page=' + page.__str__())
+
+    if page_size is not None:
+        args.append('page_size=' + page_size.__str__())
+
+    if keyword is not None:
+        args.append('keyword=' + keyword.__str__())
+
+    if order_by is not None:
+        args.append('order_by=' + order_by)
+
+    if order is not None:
+        args.append('order=' + order)
+
+    if filters.__len__() > 0:
+        args.append('filter=' + ','.join(filters))
+
+    host_url = request.host_url.rstrip('/')
+
+    ssh_keys_url = host_url + url_for('api_ssh_keys.r_get_by_filter')
+    if keyword is not None:
+        ssh_keys_url = host_url + url_for('api_ssh_keys.r_content_search')
+
+    if args.__len__() > 0:
+        ssh_keys_url = ssh_keys_url + '?' + '&'.join(args)
+
+    ssh_keys_ret = requests.get(url=ssh_keys_url, cookies=request.cookies)
+    ssh_keys_ret = json.loads(ssh_keys_ret.content)
+
+    last_page = int(ceil(ssh_keys_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 or last_page == 0:
+                break
+
+    elif last_page - page < page_length / 2:
+        for i in range(last_page - page_length + 1, last_page + 1):
+            if i < 1:
+                continue
+            pages.append(i)
+
+    else:
+        for i in range(page - page_length / 2, page + int(ceil(page_length / 2.0))):
+            pages.append(i)
+            if i == last_page or last_page == 0:
+                break
+
+    return render_template('ssh_keys_show.html', ssh_keys_ret=ssh_keys_ret,
+                           page=page, page_size=page_size, keyword=keyword, pages=pages, order_by=order_by, order=order,
+                           last_page=last_page)
+
+
+def create():
+    host_url = request.host_url.rstrip('/')
+
+    if request.method == 'POST':
+        label = request.form.get('label')
+        public_key = request.form.get('public_key', '')
+
+        payload = {
+            "label": label,
+            "public_key": public_key
+        }
+
+        url = host_url + '/api/ssh_key'
+        headers = {'content-type': 'application/json'}
+        r = requests.post(url, data=json.dumps(payload), headers=headers, cookies=request.cookies)
+        j_r = json.loads(r.content)
+        if j_r['state']['code'] != '200':
+            return render_template('failure.html',
+                                   go_back_url='/ssh_keys',
+                                   timeout=10000, title='添加失败',
+                                   message_title='添加 SSH Key 失败',
+                                   message=j_r['state']['sub']['zh-cn'])
+
+        return render_template('success.html', go_back_url='/ssh_keys', timeout=10000, title='提交成功',
+                               message_title='添加 SSH Key 的请求已被接受',
+                               message='您所提交的 SSH Key 已创建。页面将在10秒钟后自动跳转到模板列表页面!')
+
+    else:
+        return redirect(url_for('v_ssh_keys.show'))
+

+ 4 - 0
views_route_table.py

@@ -11,6 +11,7 @@ from views import dashboard
 from views import config
 from views import misc
 from views import os_template_image
+from views import ssh_key
 
 
 __author__ = 'James Iter'
@@ -47,3 +48,6 @@ add_rule_views(os_template_image.blueprint, '', views_func='os_template_image.cr
 add_rule_views(host.blueprints, '', views_func='host.show', methods=['GET'])
 add_rule_views(host.blueprint, '/detail/<node_id>', views_func='host.detail', methods=['GET'])
 
+add_rule_views(ssh_key.blueprints, '', views_func='ssh_key.show', methods=['GET'])
+add_rule_views(ssh_key.blueprint, '', views_func='ssh_key.create', methods=['POST'])
+