Debugging SSH Hosts panel.

This commit is contained in:
Pino de Candia 2018-01-09 15:19:09 -06:00
parent ae759df379
commit e3570734fa
12 changed files with 136 additions and 328 deletions

View File

@ -48,7 +48,7 @@ Howto
and should be copied to /usr/share/openstack-dashboard/openstack_dashboard/local/enabled or the and should be copied to /usr/share/openstack-dashboard/openstack_dashboard/local/enabled or the
equivalent directory for your openstack-dashboard install. equivalent directory for your openstack-dashboard install.
3. Make sure your keystone catalog contains endpoints for service type 'dns'. If no such endpoints are 3. Make sure your keystone catalog contains endpoints for service type 'ssh'. If no such endpoints are
found, the tatudashboard panels will not render. found, the tatudashboard panels will not render.
4. (Optional) Copy the tatu policy file into horizon's policy files folder, and add this config:: 4. (Optional) Copy the tatu policy file into horizon's policy files folder, and add this config::

View File

@ -1 +1,16 @@
from tatudashboard.api import tatu # noqa # (c) Copyright <year(s)> Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""REST API for Horizon dashboard Javascript code.
"""
from tatudashboard.api import passthrough

View File

@ -1,16 +0,0 @@
# (c) Copyright <year(s)> Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""REST API for Horizon dashboard Javascript code.
"""
from . import passthrough # noqa

View File

@ -1,171 +0,0 @@
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import
from keystoneauth1 import session as keystone_session
from keystoneauth1.identity import v3
from django.conf import settings # noqa
from horizon import exceptions
from openstack_dashboard.api.base import url_for # noqa
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def designateclient(request):
designate_url = ""
try:
designate_url = url_for(request, 'dns')
except exceptions.ServiceCatalogException:
LOG.debug('no dns service configured.')
return None
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
return Client(endpoint=designate_url,
token=request.user.token.id,
username=request.user.username,
tenant_id=request.user.project_id,
insecure=insecure,
cacert=cacert)
def domain_get(request, domain_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.domains.get(domain_id)
def domain_list(request):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.domains.list()
def domain_create(request, name, email, ttl=None, description=None):
d_client = designateclient(request)
if d_client is None:
return None
options = {
'description': description,
}
# TTL needs to be optionally added as argument because the client
# won't accept a None value
if ttl is not None:
options['ttl'] = ttl
domain = Domain(name=name, email=email, **options)
return d_client.domains.create(domain)
def domain_update(request, domain_id, email, ttl, description=None):
d_client = designateclient(request)
if d_client is None:
return None
# A quirk of the designate client is that you need to start with a
# base record and then update individual fields in order to persist
# the data. The designate client will only send the 'changed' fields.
domain = Domain(id=domain_id, name='', email='')
domain.email = email
domain.ttl = ttl
domain.description = description
return d_client.domains.update(domain)
def domain_delete(request, domain_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.domains.delete(domain_id)
def server_list(request, domain_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.domains.list_domain_servers(domain_id)
def record_list(request, domain_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.records.list(domain_id)
def record_get(request, domain_id, record_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.records.get(domain_id, record_id)
def record_delete(request, domain_id, record_id):
d_client = designateclient(request)
if d_client is None:
return []
return d_client.records.delete(domain_id, record_id)
def record_create(request, domain_id, **kwargs):
d_client = designateclient(request)
if d_client is None:
return []
record = Record(**kwargs)
return d_client.records.create(domain_id, record)
def record_update(request, domain_id, record_id, **kwargs):
d_client = designateclient(request)
if d_client is None:
return []
# A quirk of the designate client is that you need to start with a
# base record and then update individual fields in order to persist
# the data. The designate client will only send the 'changed' fields.
record = Record(
id=record_id,
type='A',
name='',
data='')
record.type = kwargs.get('type', None)
record.name = kwargs.get('name', None)
record.data = kwargs.get('data', None)
record.priority = kwargs.get('priority', None)
record.ttl = kwargs.get('ttl', None)
record.description = kwargs.get('description', None)
return d_client.records.update(domain_id, record)
def quota_get(request, project_id=None):
if not project_id:
project_id = request.user.project_id
d_client = designateclient(request)
return d_client.quotas.get(project_id)

View File

@ -19,7 +19,7 @@ from openstack_dashboard.dashboards.project import dashboard
class Hosts(horizon.Panel): class Hosts(horizon.Panel):
name = _("Hosts") name = _("Hosts")
slug = 'ssh_hosts' slug = 'hosts'
permissions = ('openstack.services.ssh',) permissions = ('openstack.services.ssh',)
dashboard.Project.register(Hosts) dashboard.Project.register(Hosts)

View File

@ -15,7 +15,7 @@
from tatudashboard import exceptions from tatudashboard import exceptions
# The name of the panel to be added to HORIZON_CONFIG. Required. # The name of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'ssh_hosts' PANEL = 'hosts'
# The name of the dashboard the PANEL associated with. Required. # The name of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project' PANEL_DASHBOARD = 'project'
# The name of the panel group the PANEL is associated with. # The name of the panel group the PANEL is associated with.
@ -27,6 +27,8 @@ ADD_EXCEPTIONS = {
'unauthorized': exceptions.UNAUTHORIZED, 'unauthorized': exceptions.UNAUTHORIZED,
} }
ADD_INSTALLED_APPS = ['tatudashboard']
# Python panel class of the PANEL to be added. # Python panel class of the PANEL to be added.
ADD_PANEL = ( ADD_PANEL = (
'tatudashboard.dashboards.project.hosts.panel.Hosts') 'tatudashboard.dashboards.project.hosts.panel.Hosts')

View File

@ -71,113 +71,48 @@
"properties": { "properties": {
"hostname": { "hostname": {
"type": "string", "type": "string",
"pattern": /^.+\.$/
}, },
"description": { "instance_id": {
"type": "string"
},
"email": {
"type": "string", "type": "string",
"format": "email",
"pattern": /^[^@]+@[^@]+$/
}, },
"type": { "proj_id": {
"type": "string", "type": "string",
"enum": [
"PRIMARY",
"SECONDARY"
]
}, },
"ttl": { "proj_name": {
"type": "integer", "type": "string",
"minimum": 1,
"maximum": 2147483647
}, },
"masters": { "cert": {
"type": "array", "type": "string",
"items": { },
"type": "object", "pat": {
"properties": { "type": "string",
"address": { },
"type": "string" "srv_url": {
} "type": "string",
}
},
"minItems": 1,
"uniqueItems": true
} }
} }
}, },
"form": [ "form": [
{ {
"key": "name", "key": "hostname",
"readonly": readonly, "readonly": readonly,
"title": gettext("Name"), "title": gettext("Hostname"),
"description": gettext("Host name ending in '.'"), "description": gettext("Foobar fudd"),
"validationMessage": gettext("Host must end with '.'"),
"placeholder": "example.com.",
"type": "text", "type": "text",
"required": true "required": true
}, },
{ {
"key": "description", "key": "proj_name",
"type": "textarea", "readonly": readonly,
"title": gettext("Description"), "title": gettext("Project Name"),
"description": gettext("Details about the host.") "description": gettext("Confounded JS noobie."),
},
{
"key": "email",
"title": gettext("Email Address"),
"description": gettext("Email address to contact the host owner."),
"validationMessage": gettext("Email address must contain a single '@' character"),
"type": "text", "type": "text",
"condition": "model.type == 'PRIMARY'",
"required": true "required": true
},
{
"key": "ttl",
"title": gettext("TTL"),
"description": gettext("Time To Live in seconds."),
"type": "number",
"condition": "model.type == 'PRIMARY'",
"required": true
},
{
"key": "type",
"readonly": readonly,
"title": gettext("Type"),
"description": gettext("Select the type of host"),
"type": "select",
"titleMap": [
{
"value": "PRIMARY",
"name": gettext("Primary")
},
{
"value": "SECONDARY",
"name": gettext("Secondary")
}
]
},
{
"key": "masters",
"readonly": readonly,
"title": gettext("Masters"),
"type": "array",
"description": gettext("DNS master(s) for the Secondary host."),
"condition": "model.type == 'SECONDARY'",
"add": gettext("Add Master"),
"items": [
{
"key": "masters[].address",
"title": gettext("IP Address")
}
]
} }
], ],
"model": { "model": {
"type": "PRIMARY", "proj_name": "Project24",
"ttl": 3600 "hostname": "pluto"
} }
}; };
} }

View File

@ -21,7 +21,6 @@
.factory('tatudashboard.resources.os-tatu-host.api', apiService); .factory('tatudashboard.resources.os-tatu-host.api', apiService);
apiService.$inject = [ apiService.$inject = [
'$q',
'tatudashboard.apiPassthroughUrl', 'tatudashboard.apiPassthroughUrl',
'horizon.framework.util.http.service', 'horizon.framework.util.http.service',
'horizon.framework.widgets.toast.service' 'horizon.framework.widgets.toast.service'
@ -35,7 +34,7 @@
* @description Provides direct access to Tatu Host APIs. * @description Provides direct access to Tatu Host APIs.
* @returns {Object} The service * @returns {Object} The service
*/ */
function apiService($q, apiPassthroughUrl, httpService, toastService) { function apiService(apiPassthroughUrl, httpService, toastService) {
var service = { var service = {
get: get, get: get,
list: list, list: list,
@ -51,7 +50,7 @@
/** /**
* @name list * @name list
* @description * @description
* Get a list of record sets. * Get a list of hosts.
* *
* The listing result is an object with property "items." Each item is * The listing result is an object with property "items." Each item is
* a host. * a host.
@ -62,10 +61,38 @@
* @returns {Object} The result of the API call * @returns {Object} The result of the API call
*/ */
function list(params) { function list(params) {
return httpService.get(apiPassthroughUrl + 'hosts/', params) var config = params ? {'params': params} : {};
var hosts = [{
'instance_id': '00000000-aaaa-bbbb-cccc-111122224444',
'hostname': 'fluffy',
'proj_id': 'aaaaaaaa-aaaa-bbbb-cccc-111122224444',
'proj_name': 'ProjectA',
'cert': 'Bogus ssh cert...',
'pat': '11.11.0.1:1002,11.11.0.2:1002',
'srv_url': '_ssh._tcp.fluffy.aaaaaaaa.tatu.com.',
},{
'instance_id': '11111111-aaaa-bbbb-cccc-111122224444',
'hostname': 'chester',
'proj_id': 'aaaaaaaa-aaaa-bbbb-cccc-111122224444',
'proj_name': 'ProjectA',
'cert': 'Bogus ssh cert...',
'pat': '11.11.0.1:1005,11.11.0.2:1005',
'srv_url': '_ssh._tcp.chester.aaaaaaaa.tatu.com.',
},{
'instance_id': '22222222-aaaa-bbbb-cccc-111122224444',
'hostname': 'snoopy',
'proj_id': 'aaaaaaaa-aaaa-bbbb-cccc-111122224444',
'proj_name': 'ProjectA',
'cert': 'Bogus ssh cert...',
'pat': '11.11.0.1:1009,11.11.0.2:1009',
'srv_url': '_ssh._tcp.snoopy.aaaaaaaa.tatu.com.',
}];
return hosts;
/*
return httpService.get(apiPassthroughUrl + 'hosts/', config)
.error(function () { .error(function () {
toastService.add('error', gettext('Unable to retrieve the hosts.')); toastService.add('error', gettext('Unable to retrieve the host.'));
}); });*/
} }
/** /**
@ -73,61 +100,72 @@
* @description * @description
* Get a single host by ID. * Get a single host by ID.
* *
* @param {string} hostId * @param {string} id
* Specifies the id of the host to request. * Specifies the id of the host to request.
* *
* @returns {Object} The result of the API call * @returns {Object} The result of the API call
*/ */
function get(hostId) { function get(id) {
// Unfortunately routed-details-view is not happy when load fails, which is return httpService.get(apiPassthroughUrl + 'hosts/' + id + '/')
// common when then delete action removes a record set. Mask this failure by .error(function () {
// always returning a successful promise instead of terminating the $http promise
// in the .error handler.
return httpService.get(apiPassthroughUrl + 'hosts/' + hostId + '/')
.then(undefined, function onError() {
toastService.add('error', gettext('Unable to retrieve the host.')); toastService.add('error', gettext('Unable to retrieve the host.'));
return $q.when({});
}); });
} }
/** /**
* @name delete * @name deleteHost
* @description * @description
* Delete a single host by ID * Delete a single host by ID
* @param {string} zoneId * @param id
* The id of the zone containing the host
*
* @param {string} hostId
* The id of the host within the zone
*
* @returns {*} * @returns {*}
*/ */
function deleteRecordSet(zoneId, recordSetId) { function deleteHost(id) {
return httpService.delete(apiPassthroughUrl + 'v2/zones/' + zoneId + '/recordsets/' + recordSetId + '/') return httpService.delete(apiPassthroughUrl + 'hosts/' + id + '/')
.error(function () { .error(function () {
toastService.add('error', gettext('Unable to delete the host.')); toastService.add('error', gettext('Unable to delete the host.'));
}); });
} }
function create(zoneId, data) { /**
return httpService.post(apiPassthroughUrl + 'v2/zones/' + zoneId + '/recordsets/', data) * @name create
.error(function () { * @description
* Create a host
*
* @param {Object} data
* Specifies the host information to create
*
* @returns {Object} The created host object
*/
function create(data) {
return httpService.post(apiPassthroughUrl + 'hosts/', data)
.error(function() {
toastService.add('error', gettext('Unable to create the host.')); toastService.add('error', gettext('Unable to create the host.'));
}); })
} }
function update(zoneId, recordSetId, data) { /**
* @name create
* @description
* Update a host
*
* @param {Object} id - host id
* @param {Object} data to pass directly to host update API
* Specifies the host information to update
*
* @returns {Object} The updated host object
*/
function update(id, data) {
// The update API will not accept extra data. Restrict the input to only the allowed // The update API will not accept extra data. Restrict the input to only the allowed
// fields // fields
var apiData = { var apiData = {
email: data.email,
ttl: data.ttl, ttl: data.ttl,
description: data.description, description: data.description
records: data.records
}; };
return httpService.put(apiPassthroughUrl + 'v2/zones/' + zoneId + '/recordsets/' + recordSetId, apiData) return httpService.patch(apiPassthroughUrl + 'hosts/' + id + '/', apiData )
.error(function () { .error(function() {
toastService.add('error', gettext('Unable to update the host.')); toastService.add('error', gettext('Unable to update the host.'));
}); })
} }
} }
}()); }());

View File

@ -32,24 +32,22 @@
'tatudashboard.resources.os-tatu-host.details' 'tatudashboard.resources.os-tatu-host.details'
]) ])
.constant( .constant(
'tatudashboard.resources.os-tatu-host.resourceType', 'tatudashboard.resources.os-tatu-host.resourceType', 'OS::Tatu::Host')
'OS::Tatu::Host')
.config(config) .config(config)
.run(run); .run(run);
config.$inject = ['$provide', '$windowProvider']; config.$inject = ['$provide', '$windowProvider'];
function config($provide, $windowProvider) { function config($provide, $windowProvider) {
var path = $windowProvider.$get().STATIC_URL + 'tatudashboard/resources/os-tatu-recordset/'; var path = $windowProvider.$get().STATIC_URL + 'tatudashboard/resources/os-tatu-host/';
$provide.constant('tatudashboard.resources.os-tatu-recordset.basePath', path); $provide.constant('tatudashboard.resources.os-tatu-host.basePath', path);
} }
run.$inject = [ run.$inject = [
'horizon.app.core.detailRoute', 'horizon.app.core.detailRoute',
'horizon.framework.conf.resource-type-registry.service', 'horizon.framework.conf.resource-type-registry.service',
'tatudashboard.resources.os-tatu-recordset.api', 'tatudashboard.resources.os-tatu-host.api',
'tatudashboard.resources.os-tatu-recordset.resourceType', 'tatudashboard.resources.os-tatu-host.resourceType',
'tatudashboard.resources.os-tatu-recordset.typeMap',
'tatudashboard.resources.util' 'tatudashboard.resources.util'
]; ];
@ -57,26 +55,26 @@
registry, registry,
hostApi, hostApi,
resourceTypeString, resourceTypeString,
typeMap,
util) { util) {
var resourceType = registry.getResourceType(resourceTypeString); var resourceType = registry.getResourceType(resourceTypeString);
resourceType resourceType
.setNames(gettext('SSH Host'), gettext('SSH Hosts')) .setNames(gettext('SSH Host'), gettext('SSH Hosts'))
.setListFunction(list) .setListFunction(listHosts)
.setProperty('action', {
label: gettext('Action'),
values: util.actionMap()
})
.setProperty('instance_id', { .setProperty('instance_id', {
label: gettext('Instance ID') label: gettext('Instance ID')
}) })
.setProperty('hostname', { .setProperty('hostname', {
label: gettext('Hostname'), label: gettext('Hostname'),
filters: ['noName']
}) })
.setProperty('proj_id', { .setProperty('proj_id', {
label: gettext('Project ID'), label: gettext('Project ID'),
filters: ['noValue']
}) })
.setProperty('proj_name', { .setProperty('proj_name', {
label: gettext('Project Name'), label: gettext('Project Name'),
filters: ['noValue']
}) })
.setProperty('cert', { .setProperty('cert', {
label: gettext('Certificate') label: gettext('Certificate')
@ -91,10 +89,10 @@
resourceType resourceType
.tableColumns .tableColumns
.append({ .append({
id: 'host_name', id: 'hostname',
priority: 1, priority: 1,
sortDefault: true, sortDefault: true,
template: '<a ng-href="{$ \'' + detailRoute + 'OS::Tatu::Host/\' + item.id $}">{$ item.name $}</a>' template: '<a ng-href="{$ \'' + detailRoute + 'OS::Tatu::Host/\' + item.instance_id $}">{$ item.hostname $}</a>'
}) })
.append({ .append({
id: 'proj_name', id: 'proj_name',
@ -112,8 +110,15 @@
resourceType resourceType
.filterFacets .filterFacets
.append({ .append({
label: gettext('Name'), label: gettext('Hostname'),
name: 'name', name: 'hostname',
isServer: false,
singleton: true,
persistent: false
})
.append({
label: gettext('Project'),
name: 'proj_name',
isServer: false, isServer: false,
singleton: true, singleton: true,
persistent: false persistent: false

View File

@ -58,7 +58,7 @@
$provide.constant('tatudashboard.basePath', path); $provide.constant('tatudashboard.basePath', path);
$routeProvider $routeProvider
.when('/project/ssh_hosts/', { .when('/project/hosts/', {
templateUrl: path + 'hosts.html' templateUrl: path + 'hosts.html'
}); });
} }