James Iter 7 年之前
父節點
當前提交
7bea988031

+ 8 - 0
jimvc/__init__.py

@@ -67,6 +67,10 @@ from jimvc.api.host_performance import blueprints as host_performance_blueprints
 from jimvc.api.dashboard import blueprint as dashboard_blueprint
 from jimvc.api.dashboard import blueprints as dashboard_blueprints
 from jimvc.api.about import blueprint as about_blueprint
+from jimvc.api.project import blueprint as project_blueprint
+from jimvc.api.project import blueprints as project_blueprints
+from jimvc.api.service import blueprint as service_blueprint
+from jimvc.api.service import blueprints as service_blueprints
 
 from jimvc.views.error_pages import *
 from jimvc.views.config import blueprint as view_config_blueprint
@@ -272,6 +276,10 @@ try:
     app.register_blueprint(dashboard_blueprint)
     app.register_blueprint(dashboard_blueprints)
     app.register_blueprint(about_blueprint)
+    app.register_blueprint(project_blueprint)
+    app.register_blueprint(project_blueprints)
+    app.register_blueprint(service_blueprint)
+    app.register_blueprint(service_blueprints)
 
     app.register_blueprint(view_config_blueprint)
     app.register_blueprint(view_misc_blueprint)

+ 20 - 2
jimvc/api/guest.py

@@ -16,7 +16,7 @@ from flask import Blueprint, url_for, request
 
 from jimvc.api.base import Base
 from jimvc.models.initialize import dev_table
-from jimvc.models import app_config, GuestState
+from jimvc.models import app_config, GuestState, Service
 from jimvc.models import DiskState, Host
 from jimvc.models import Database as db
 from jimvc.models import Config
@@ -83,6 +83,11 @@ def r_create():
             Rules.SSH_KEYS_ID.value
         )
 
+    if 'service_id' in request.json:
+        args_rules.append(
+            Rules.SERVICE_ID.value
+        )
+
     try:
         ret = dict()
         ret['state'] = ji.Common.exchange_state(20000)
@@ -149,6 +154,11 @@ def r_create():
             for row in rows:
                 ssh_keys.append(row['public_key'])
 
+        # 确保目标 服务组 存在
+        service = Service()
+        service.id = request.json.get('service_id', 1)
+        service.get()
+
         bandwidth = request.json.get('bandwidth')
         bandwidth_unit = request.json.get('bandwidth_unit')
 
@@ -222,6 +232,7 @@ def r_create():
                 chosen_host = available_hosts_mapping_by_node_id[node_id]
 
             guest.node_id = chosen_host['node_id']
+            guest.service_id = service.id
 
             guest_xml = GuestXML(host=chosen_host, guest=guest, disk=disk, config=config,
                                  os_type=os_template_profile.os_type)
@@ -1094,7 +1105,8 @@ def r_distribute_count():
 def r_update(uuid):
 
     args_rules = [
-        Rules.UUID.value
+        Rules.UUID.value,
+        Rules.SERVICE_ID.value
     ]
 
     if 'remark' in request.json:
@@ -1102,6 +1114,11 @@ def r_update(uuid):
             Rules.REMARK.value,
         )
 
+    if 'service_id' in request.json:
+        args_rules.append(
+            Rules.SERVICE_ID.value,
+        )
+
     if args_rules.__len__() < 2:
         ret = dict()
         ret['state'] = ji.Common.exchange_state(20000)
@@ -1116,6 +1133,7 @@ def r_update(uuid):
         guest.get_by('uuid')
 
         guest.remark = request.json.get('remark', guest.label)
+        guest.service_id = request.json.get('service_id', guest.service_id)
 
         guest.update()
         guest.get()

+ 159 - 0
jimvc/api/project.py

@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+import json
+from flask import Blueprint
+from flask import request
+
+from jimvc.api.base import Base
+from jimvc.models import Utils
+from jimvc.models import Rules
+from jimvc.models import Project
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_project',
+    __name__,
+    url_prefix='/api/project'
+)
+
+blueprints = Blueprint(
+    'api_projects',
+    __name__,
+    url_prefix='/api/projects'
+)
+
+
+project_base = Base(the_class=Project, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.NAME.value
+    ]
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        project = Project()
+        project.name = request.json.get('name', None)
+        project.description = request.json.get('description', '')
+
+        project.create()
+        project.get_by('create_time')
+
+        ret['data'] = project.__dict__
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(_id):
+
+    project = Project()
+
+    args_rules = [
+        Rules.ID.value
+    ]
+
+    if 'name' in request.json:
+        args_rules.append(
+            Rules.NAME.value,
+        )
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.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)
+        project.id = int(request.json.get('id'))
+
+        project.get()
+        project.name = request.json.get('name', project.name)
+        project.description = request.json.get('description', project.description)
+
+        project.update()
+        project.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = project.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(ids):
+    return project_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return project_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return project_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(ids):
+
+    args_rules = [
+        Rules.IDS.value
+    ]
+
+    try:
+        ji.Check.previewing(args_rules, {'ids': ids})
+
+        project = Project()
+
+        # 检测所指定的 项目 都存在
+        for _id in ids.split(','):
+            project.id = int(_id)
+            project.get()
+
+        # 执行删除操作
+        for _id in ids.split(','):
+            project.id = int(_id)
+            project.get()
+            project.delete()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 167 - 0
jimvc/api/service.py

@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+import json
+from flask import Blueprint
+from flask import request
+
+from jimvc.api.base import Base
+from jimvc.models import Utils
+from jimvc.models import Rules
+from jimvc.models import Service
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+blueprint = Blueprint(
+    'api_service',
+    __name__,
+    url_prefix='/api/service'
+)
+
+blueprints = Blueprint(
+    'api_services',
+    __name__,
+    url_prefix='/api/services'
+)
+
+
+service_base = Base(the_class=Service, the_blueprint=blueprint, the_blueprints=blueprints)
+
+
+@Utils.dumps2response
+def r_create():
+
+    args_rules = [
+        Rules.PROJECT_ID.value,
+        Rules.NAME.value
+    ]
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.value,
+        )
+
+    try:
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+
+        ji.Check.previewing(args_rules, request.json)
+
+        service = Service()
+        service.project_id = request.json.get('project_id', None)
+        service.name = request.json.get('name', None)
+        service.description = request.json.get('description', '')
+
+        service.create()
+        service.get_by('create_time')
+
+        ret['data'] = service.__dict__
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_update(_id):
+
+    service = Service()
+
+    args_rules = [
+        Rules.ID.value
+    ]
+
+    if 'project_id' in request.json:
+        args_rules.append(
+            Rules.PROJECT_ID.value,
+        )
+
+    if 'name' in request.json:
+        args_rules.append(
+            Rules.NAME.value,
+        )
+
+    if 'description' in request.json:
+        args_rules.append(
+            Rules.DESCRIPTION.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)
+        service.id = int(request.json.get('id'))
+
+        service.get()
+        service.project_id = request.json.get('project_id', service.project_id)
+        service.name = request.json.get('name', service.name)
+        service.description = request.json.get('description', service.description)
+
+        service.update()
+        service.get()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        ret['data'] = service.__dict__
+        return ret
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+
+
+@Utils.dumps2response
+def r_get(ids):
+    return service_base.get(ids=ids, ids_rule=Rules.IDS.value, by_field='id')
+
+
+@Utils.dumps2response
+def r_get_by_filter():
+    return service_base.get_by_filter()
+
+
+@Utils.dumps2response
+def r_content_search():
+    return service_base.content_search()
+
+
+@Utils.dumps2response
+def r_delete(ids):
+
+    args_rules = [
+        Rules.IDS.value
+    ]
+
+    try:
+        ji.Check.previewing(args_rules, {'ids': ids})
+
+        service = Service()
+
+        # 检测所指定的 项目 都存在
+        for _id in ids.split(','):
+            service.id = int(_id)
+            service.get()
+
+        # 执行删除操作
+        for _id in ids.split(','):
+            service.id = int(_id)
+            service.get()
+            service.delete()
+
+        ret = dict()
+        ret['state'] = ji.Common.exchange_state(20000)
+        return ret
+
+    except ji.PreviewingError, e:
+        return json.loads(e.message)
+

+ 35 - 3
jimvc/api_route_table.py

@@ -3,9 +3,25 @@
 
 
 from jimvc.models import add_rule_api
-from jimvc.api import config, os_template_profile, snapshot, os_template_initialize_operate, user, ssh_key, \
-    os_template_image, guest_performance, disk, log, os_template_initialize_operate_set, dashboard, guest, host, \
-    host_performance, about
+from jimvc.api import config
+from jimvc.api import os_template_profile
+from jimvc.api import snapshot
+from jimvc.api import os_template_initialize_operate
+from jimvc.api import user
+from jimvc.api import ssh_key
+from jimvc.api import os_template_image
+from jimvc.api import guest_performance
+from jimvc.api import disk
+from jimvc.api import log
+from jimvc.api import os_template_initialize_operate_set
+from jimvc.api import dashboard
+from jimvc.api import guest
+from jimvc.api import host
+from jimvc.api import host_performance
+from jimvc.api import about
+from jimvc.api import project
+from jimvc.api import service
+
 
 __author__ = 'James Iter'
 __date__ = '2017/03/30'
@@ -168,6 +184,22 @@ add_rule_api(snapshot.blueprint, '/_convert_to_os_template_image/<snapshot_id>/<
              api_func='snapshot.r_convert_to_os_template_image', methods=['PUT'])
 add_rule_api(snapshot.blueprints, '/_show', api_func='snapshot.r_show', methods=['GET'])
 
+# 项目操作
+add_rule_api(project.blueprint, '', api_func='project.r_create', methods=['POST'])
+add_rule_api(project.blueprint, '/<_id>', api_func='project.r_update', methods=['PATCH'])
+add_rule_api(project.blueprints, '/<ids>', api_func='project.r_delete', methods=['DELETE'])
+add_rule_api(project.blueprints, '/<ids>', api_func='project.r_get', methods=['GET'])
+add_rule_api(project.blueprints, '', api_func='project.r_get_by_filter', methods=['GET'])
+add_rule_api(project.blueprints, '/_search', api_func='project.r_content_search', methods=['GET'])
+
+# 服务组操作
+add_rule_api(service.blueprint, '', api_func='service.r_create', methods=['POST'])
+add_rule_api(service.blueprint, '/<_id>', api_func='service.r_update', methods=['PATCH'])
+add_rule_api(service.blueprints, '/<ids>', api_func='service.r_delete', methods=['DELETE'])
+add_rule_api(service.blueprints, '/<ids>', api_func='service.r_get', methods=['GET'])
+add_rule_api(service.blueprints, '', api_func='service.r_get_by_filter', methods=['GET'])
+add_rule_api(service.blueprints, '/_search', api_func='service.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'])

+ 6 - 1
jimvc/models/__init__.py

@@ -46,6 +46,11 @@ from guest import (
     GuestMigrateInfo
 )
 
+from project import (
+    Project,
+    Service
+)
+
 from ssh_key import (
     SSHKey
 )
@@ -133,6 +138,6 @@ __all__ = [
     'OSTemplateInitializeOperate', 'EventProcessor', 'ResponseState', 'GuestCPUMemory', 'GuestTraffic', 'GuestDiskIO',
     'HostCPUMemory', 'HostTraffic', 'HostDiskUsageIO', 'Host', 'Snapshot', 'SnapshotDiskMapping', 'OSTemplateImageKind',
     'GuestMigrateInfo', 'GuestCollectionPerformanceDataKind', 'HostCollectionPerformanceDataKind', 'StorageMode',
-    'OSTemplateInitializeOperateKind', 'dev_table'
+    'OSTemplateInitializeOperateKind', 'dev_table', 'Project', 'Service'
 ]
 

+ 3 - 1
jimvc/models/guest.py

@@ -32,6 +32,7 @@ class Guest(ORM):
         self.status = GuestState.no_state.value
         self.progress = 0
         self.node_id = None
+        self.service_id = 1
         self.cpu = None
         self.memory = None
         self.bandwidth = 0
@@ -51,13 +52,14 @@ class Guest(ORM):
             'status': FilterFieldType.INT.value,
             'remark': FilterFieldType.STR.value,
             'node_id': FilterFieldType.INT.value,
+            'service_id': FilterFieldType.INT.value,
             'ip': FilterFieldType.STR.value,
             'bandwidth': FilterFieldType.INT.value
         }
 
     @staticmethod
     def get_allow_update_keywords():
-        return ['remark', 'cpu', 'memory', 'bandwidth', 'network', 'manage_network', 'vnc_password']
+        return ['remark', 'cpu', 'memory', 'bandwidth', 'network', 'manage_network', 'vnc_password', 'service_id']
 
     @staticmethod
     def get_allow_content_search_keywords():

+ 72 - 0
jimvc/models/project.py

@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+import jimit as ji
+
+from filter import FilterFieldType
+from orm import ORM
+
+
+__author__ = 'James Iter'
+__date__ = '2018/10/7'
+__contact__ = 'james.iter.cn@gmail.com'
+__copyright__ = '(c) 2018 by James Iter.'
+
+
+class Project(ORM):
+
+    _table_name = 'project'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(Project, self).__init__()
+        self.id = 0
+        self.name = ''
+        self.description = ''
+        self.create_time = ji.Common.tus()
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'name': FilterFieldType.STR.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return []
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['name']
+
+
+class Service(ORM):
+
+    _table_name = 'service'
+    _primary_key = 'id'
+
+    def __init__(self):
+        super(Service, self).__init__()
+        self.id = 0
+        self.project_id = 0
+        self.name = ''
+        self.description = ''
+        self.create_time = ji.Common.tus()
+
+    @staticmethod
+    def get_filter_keywords():
+        return {
+            'id': FilterFieldType.INT.value,
+            'project_id': FilterFieldType.INT.value,
+            'name': FilterFieldType.STR.value
+        }
+
+    @staticmethod
+    def get_allow_update_keywords():
+        return ['project_id']
+
+    @staticmethod
+    def get_allow_content_search_keywords():
+        return ['name']

+ 4 - 0
jimvc/models/rules.py

@@ -123,3 +123,7 @@ class Rules(Enum):
 
     TOKEN = (basestring, 'token')
 
+    PROJECT_ID = (int, 'project_id')
+    PROJECTS_ID = (REG_NUMBERS, 'projects_id')
+    SERVICE_ID = (int, 'service_id')
+    SERVICES_ID = (REG_NUMBERS, 'services_id')

+ 380 - 4
jimvc/themes/default/templates/guests_show.html

@@ -80,6 +80,57 @@
 
         .btn-ability-line {
         }
+
+        .project {
+            position: relative;
+            float: left;
+            display: block;
+            margin: 0 10px 20px 10px;
+            width: 338px;
+            height: 208px;
+            border-radius: 8px;
+            background-color: #F6F6F6;
+        }
+
+        .project:hover {
+            background-color: #EFEFEF;
+        }
+
+        .project:hover > .project_name {
+            color: #0275d8;
+        }
+
+        .project_name {
+            font-size: 36px;
+            color: #292b2c;
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+        }
+
+        .project-nav-tabs {
+            font-size: smaller;
+        }
+
+        .project-nav-tabs > .active > a {
+            background: #379cfb !important;
+        }
+
+        .project-service-name {
+            font-size: 24px;
+            font-weight: bold;
+            line-height: 1.25;
+            margin-top: 24px;
+            margin-bottom: 16px;
+        }
+
+        .table-bordered-project-service > tbody > tr {
+            height: 70px;
+        }
     </style>
 
 {% endblock head %}
@@ -92,6 +143,7 @@
     var resource_path = window.location.pathname;
     var cur_url = resource_path;
     var show_real_password_for_renew = false;
+    var sheet_tag = window.location.hash;
 
     $(document).ready(function() {
         page_size = $('#page_size').val();
@@ -283,6 +335,8 @@
             $('#batch_adjust_ability_form_button').prop('disabled', true);
             $('#batch_adjust_ability_modal .form-group').removeClass('has-success');
         });
+
+        nav_to_sheet_tag();
     });
 
     function refresh() {
@@ -791,6 +845,18 @@
         }
     }
 
+    function change_display_password_for_row(me) {
+        if ($(me).hasClass("icon-elusive-eye")) {
+            $(me).removeClass("icon-elusive-eye").addClass("icon-elusive-eye-off");
+            $(me).parent().children(".real_password").toggle();
+            $(me).parent().children(".unreal_password").toggle();
+        } else {
+            $(me).removeClass("icon-elusive-eye-off").addClass("icon-elusive-eye");
+            $(me).parent().children(".unreal_password").toggle();
+            $(me).parent().children(".real_password").toggle();
+        }
+    }
+
     function change_display_password_for_renew(me) {
         if (show_real_password_for_renew) {
             show_real_password_for_renew = false;
@@ -802,14 +868,137 @@
             $('#new_password').attr({type: "text"});
         }
     }
+
+    function select_this(me) {
+        $(me).parent().parent().children().removeClass('active');
+        $(me).parent().addClass('active');
+
+        $('#view_by_original, #view_by_project').css('display', 'none');
+
+        if (me.id === 'view_by_project_label') {
+            $('#view_by_project').css('display', 'grid');
+            history.replaceState(null, null, window.location.pathname);
+            window.location.hash = "project";
+            refresh_project();
+        } else {
+            $('#view_by_original').css('display', 'unset');
+            history.replaceState(null, null, ' ');
+        }
+    }
+
+    function nav_to_sheet_tag() {
+        var view_by_original_label = $('#view_by_original_label');
+        var view_by_project_label = $('#view_by_project_label');
+
+        view_by_original_label.parent().parent().children().removeClass('active');
+
+        $('#view_by_original, #view_by_project').css('display', 'none');
+
+        if (sheet_tag.indexOf('#project') != -1) {
+            view_by_project_label.parent().addClass('active');
+            $('#view_by_project').css('display', 'grid');
+            refresh_project();
+        } else {
+            view_by_original_label.parent().addClass('active');
+            $('#view_by_original').css('display', 'unset');
+        }
+    }
+
+    function go_into_project(me) {
+        var project_id = $(me).data('project_id');
+        window.location.hash = ['project', project_id].join('#');
+        refresh_project();
+    }
+
+    function refresh_project_cards() {
+        $.ajax({
+            url : '/api/projects',
+            type : 'GET',
+            contentType: "application/json; charset=utf-8",
+            dataType: 'json',
+            error : function() {
+            },
+            success : function(data, textStatus, xhr) {
+                $('#project_tool_bar').css('display', 'unset');
+                $('#project_cards').css('display', 'unset');
+                $('#project_cards').empty();
+                $.each(data.data, function(k, v) {
+                    $('#project_cards').append(
+                        '<a class="project" href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '"><span class="project_name">' + v['name'] + '</span></a>'
+                    );
+                });
+            }
+        });
+    }
+
+    function refresh_services(project_id) {
+        $('#project_services').css('display', 'unset');
+    }
+
+    function refresh_project() {
+        var project_id = window.location.hash.replace(/\#/, "").split('#');
+
+        $('#project_tool_bar').css('display', 'none');
+        $('#project_cards').css('display', 'none');
+        $('#project_tabs').css('display', 'none');
+        $('#project_desc').css('display', 'none');
+        $('#project_services').css('display', 'none');
+
+        if (project_id.length === 2) {
+            project_id = project_id[1];
+
+            $.ajax({
+                url : '/api/projects',
+                type : 'GET',
+                contentType: "application/json; charset=utf-8",
+                dataType: 'json',
+                error : function() {
+                },
+                success : function(data, textStatus, xhr) {
+                    $('#project_tabs').css('display', 'unset');
+                    $('#project_tabs').empty();
+                    $('#project_tabs').append('<ul class="nav nav-tabs project-nav-tabs mrg25B"></ul>');
+                    $.each(data.data, function(k, v) {
+                        if (v['id'].toString() === project_id) {
+                            $('#project_tabs > ul').append(
+                                '<li class="active"><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
+                            );
+                        } else {
+                            $('#project_tabs > ul').append(
+                                '<li><a href="javascript:;" onclick="go_into_project(this);" data-project_id="' + v['id'] + '">' + v['name'] + '</a></li>'
+                            );
+                        }
+                    });
+                }
+            });
+
+            $.ajax({
+                url : '/api/projects/' + project_id,
+                type : 'GET',
+                contentType: "application/json; charset=utf-8",
+                dataType: 'json',
+                error : function() {
+                },
+                success : function(data, textStatus, xhr) {
+                    $('#project_desc').css('display', 'unset');
+                    $('#project_desc').text(data.data['description']);
+                }
+            });
+
+            refresh_services(project_id);
+        } else {
+            refresh_project_cards();
+        }
+    }
 </script>
 <div class="panel">
     <div class="panel-body">
-        <h3 class="title-hero" style="font-size: 24px;">
-            虚拟机实例
-        </h3>
+        <ul class="nav nav-tabs mrg25B">
+            <li class="active"><a href="javascript:;" onclick="select_this(this);" id="view_by_original_label">原始视图</a></li>
+            <li><a href="javascript:;" onclick="select_this(this);" id="view_by_project_label">项目视图</a></li>
+        </ul>
         <div>
-            <div id="datatable-row-highlight_wrapper" class="dataTables_wrapper form-inline">
+            <div id="view_by_original" 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;">
@@ -1087,6 +1276,193 @@
                     </tfoot>
                 </table>
             </div>
+            <div id="view_by_project" class="dataTables_wrapper form-inline" style="display: none;">
+                <div id="project_tool_bar" class="row" style="padding: 10px 10px 10px 0; width: 100%; display: none;">
+                    <div class="col-sm-12" style="padding-right: 0;">
+                        <div class="pull-right">
+                            <a class="btn btn-info add-page-transition" href="/guests/create" data-transition="pt-page-moveFromRight-init" style="border-radius: 0; padding-left: 40px; padding-right: 40px;">新建项目</a>
+                        </div>
+                    </div>
+                </div>
+                <div style="margin-top: 20px;">
+                    <div id="project_tabs" style="display: none;"></div>
+                    <div id="project_desc" style="display: none;"></div>
+                    <div id="project_services" style="display: none;">
+                        <div id="project_service_name" class="project-service-name">Web</div>
+                        <div id="project_service_guests">
+                            <table id="guest_list" class="table-hover table-bordered-project-service" cellspacing="0" width="100%" role="grid"
+                                   style="width: 100%; margin-bottom: 4px; border-bottom-width: 0;">
+                                <tbody>
+                                {% for item in guests %}
+                                    <tr role="row" class="odd" onmouseover="row_onmouseover(this);" onmouseout="row_onmouseout(this);">
+                                        <td style="display: none;">{{ item.uuid }}</td>
+                                        <td><input title="选中" type="checkbox"></td>
+                                        <td width="180px;">
+                                            <div>
+                                                <a href="/guest/detail/{{ item.uuid }}">{{ item.label }}</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 width="40px" style="padding-left: 12px;">
+                                            {% if os_templates_image_mapping_by_id[item.os_template_image_id | string].logo == "" %}
+                                                <span class="{{ os_templates_profile_mapping_by_id[os_templates_image_mapping_by_id[item.os_template_image_id | string].os_template_profile_id].icon }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
+                                            {% else %}
+                                                <span class="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].logo }}" title="{{ os_templates_image_mapping_by_id[item.os_template_image_id | string].label }}"></span>
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ format_guest_status(item.status, item.progress)|safe }}</td>
+                                        <td>{{ hosts_mapping_by_node_id[item.node_id | string].hostname }}</td>
+                                        <td>
+                                            CPU :&nbsp;&nbsp;{{ item.cpu }}&nbsp;核<br />
+                                            内存 :&nbsp;&nbsp;{{ item.memory }}&nbsp;GB<br />
+                                            带宽 :&nbsp;
+                                            {% if item.bandwidth == 0 %}
+                                                <span style="font-size: 16px;" title="无限带宽">
+                                &nbsp;∞
+                                </span>
+                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
+                                                {{ item.bandwidth // 1000 }}
+
+                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
+                                                {{ item.bandwidth // 1000**2 }}
+
+                                            {% else %}
+                                                {{ item.bandwidth // 1000**3 }}
+
+                                            {% endif %}
+
+                                            {% if item.bandwidth == 0 %}
+                                            {% elif item.bandwidth > 0 and item.bandwidth < 1000**2 %}
+                                                Kbps
+                                            {% elif item.bandwidth >= 1000**2 and item.bandwidth < 1000**3 %}
+                                                Mbps
+                                            {% else %}
+                                                Gbps
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ item.ip }}</td>
+                                        <td width="150px;"><a href="#" class="glyph-icon icon-elusive-eye-off" onclick="change_display_password_for_row(this);" style="color: #014c8c;"></a><span class="unreal_password">******</span><span class="real_password" style="display: none;">{{ item.password }}</span></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.status not in [5, 6, 7] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#boot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#boot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            启动
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#reboot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            重启
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_reboot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#force_reboot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            强制重启
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#shutdown_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            停止
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#force_shutdown_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#force_shutdown_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            强制停止
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if not item.snapshot.creatable %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#create_snapshot_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#create_snapshot_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent);
+                                                $('#snapshot_label').val($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            创建快照
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#suspend_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#suspend_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            暂停
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [4] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#resume_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#resume_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            恢复
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [1, 2] %} disabled {% endif %}">
+                                                        <a href="/guest/vnc/{{ item.uuid }}" target="_blank">远程连接</a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#allocate_bandwidth_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#allocate_bandwidth_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            分配带宽
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [6] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#adjust_ability_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#adjust_ability_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            变更配置
+                                                        </a>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [2] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#reset_password_modal"
+                                                           onclick="$('#reset_password_type').val('single')">
+                                                            密码重置
+                                                        </a>
+                                                    </li>
+                                                    <li class="{% if item.status not in [2, 4, 5, 6, 7] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#migrate_modal"
+                                                           onclick="$('#migrate_type').val('single')">
+                                                            迁移到
+                                                        </a>
+                                                    </li>
+                                                    <li class="divider"></li>
+                                                    <li class="{% if item.status not in [5, 6, 7, 255] %} disabled {% endif %}">
+                                                        <a href="javascript:;" data-toggle="modal" data-target="#delete_modal"
+                                                           onclick="$('#instance_uuid').val($($(this).parent().parent().parent().parent().parent().children()[0]).text());
+                                                $('#delete_instance_desc').text($($(this).parent().parent().parent().parent().parent().children()[2]).find('div a')[0].textContent + '/' + $($(this).parent().parent().parent().parent().parent().children()[2]).find('div p')[0].textContent)">
+                                                            删除
+                                                        </a>
+                                                    </li>
+                                                </ul>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                {% endfor %}
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                    <div id="project_cards" style="display: none;"></div>
+                </div>
+            </div>
         </div>
     </div>
 </div>

+ 32 - 0
misc/init.sql

@@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS guest(
     status TINYINT UNSIGNED NOT NULL DEFAULT 0,
     progress TINYINT UNSIGNED NOT NULL DEFAULT 0,
     node_id BIGINT UNSIGNED NOT NULL,
+    service_id BIGINT UNSIGNED NOT NULL default 1,
     cpu TINYINT UNSIGNED NOT NULL,
     memory INT UNSIGNED NOT NULL,
     -- bps
@@ -56,6 +57,7 @@ CREATE TABLE IF NOT EXISTS guest(
 ALTER TABLE guest ADD INDEX (uuid);
 ALTER TABLE guest ADD INDEX (label);
 ALTER TABLE guest ADD INDEX (node_id);
+ALTER TABLE guest ADD INDEX (service_id);
 ALTER TABLE guest ADD INDEX (ip);
 ALTER TABLE guest ADD INDEX (remark);
 
@@ -420,6 +422,36 @@ ALTER TABLE snapshot_disk_mapping ADD UNIQUE INDEX (snapshot_id, disk_uuid);
 ALTER TABLE snapshot_disk_mapping ADD INDEX (disk_uuid);
 
 
+CREATE TABLE IF NOT EXISTS project(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE project ADD INDEX (name);
+
+
+CREATE TABLE IF NOT EXISTS service(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    project_id BIGINT UNSIGNED NOT NULL,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE service ADD INDEX (project_id);
+ALTER TABLE service ADD INDEX (name);
+
+
+INSERT INTO project (name, description, create_time) VALUES ('我的项目', '由 JimV 创建的默认项目。', UNIX_TIMESTAMP(NOW()) * 1000000);
+INSERT INTO service (project_id, name, description, create_time) VALUES (1, '服务组', '由 JimV 创建的默认服务组。', UNIX_TIMESTAMP(NOW()) * 1000000);
+
+
 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);

+ 34 - 0
misc/v0.6_to_v0.7/update.sql

@@ -0,0 +1,34 @@
+
+CREATE TABLE IF NOT EXISTS project(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE project ADD INDEX (name);
+
+
+CREATE TABLE IF NOT EXISTS service(
+    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+    project_id BIGINT UNSIGNED NOT NULL,
+    name VARCHAR(127) NOT NULL,
+    description TEXT,
+    create_time BIGINT UNSIGNED NOT NULL,
+    PRIMARY KEY (id))
+    ENGINE=Innodb
+    DEFAULT CHARSET=utf8;
+
+ALTER TABLE service ADD INDEX (project_id);
+ALTER TABLE service ADD INDEX (name);
+
+
+INSERT INTO project (name, description, create_time) VALUES ('我的项目', '由 JimV 创建的默认项目。', UNIX_TIMESTAMP(NOW()) * 1000000);
+INSERT INTO service (project_id, name, description, create_time) VALUES (1, '服务组', '由 JimV 创建的默认服务组。', UNIX_TIMESTAMP(NOW()) * 1000000);
+
+
+ALTER TABLE guest ADD COLUMN service_id BIGINT UNSIGNED NOT NULL DEFAULT 1;
+ALTER TABLE guest ADD INDEX (service_id);
+