Merge "Add keypair support"

This commit is contained in:
Jenkins 2017-04-20 11:48:20 +00:00 committed by Gerrit Code Review
commit 0d00ed2fa8
23 changed files with 1018 additions and 1 deletions

View File

@ -0,0 +1,155 @@
.. -*- rst -*-
=====================
Keypairs (keypairs)
=====================
Generates, imports, and deletes SSH keys.
List Keypairs
=============
.. rest_method:: GET /keypairs
Lists keypairs that are associated with the account.
Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403)
Request
-------
.. rest_parameters:: parameters.yaml
- user_id: keypair_user
Response
--------
.. rest_parameters:: parameters.yaml
- created_at: created_at
- updated_at: created_at
- keypairs: keypairs
- user_id: keypair_userid_in
- name: keypair_name
- public_key: keypair_public_key
- fingerprint: keypair_fingerprint
- type: keypair_type
- links: links
**Example List Keypairs: JSON response**
.. literalinclude:: samples/keypairs/keypairs-list-resp.json
:language: javascript
Create Or Import Keypair
========================
.. rest_method:: POST /keypairs
Generates or imports a keypair.
Normal response codes: 200, 201
.. note::
The success status code was changed from 200 to 201 in version 2.2
Error response codes: badRequest(400), unauthorized(401), forbidden(403), conflict(409)
Request
-------
.. rest_parameters:: parameters.yaml
- name: keypair_name
- public_key: keypair_public_key_in
- type: keypair_type_in
- user_id: keypair_userid_in
**Example Create Or Import Keypair: JSON request**
.. literalinclude:: samples/keypairs/keypairs-import-post-req.json
:language: javascript
Response
--------
.. rest_parameters:: parameters.yaml
- created_at: created_at
- updated_at: created_at
- name: keypair_name
- public_key: keypair_public_key
- fingerprint: keypair_fingerprint
- user_id: keypair_userid
- private_key: keypair_private_key
- type: keypair_type
**Example Create Or Import Keypair: JSON response**
.. literalinclude:: samples/keypairs/keypairs-import-post-resp.json
:language: javascript
Show Keypair Details
====================
.. rest_method:: GET /keypairs/{keypair_name}
Shows details for a keypair that is associated with the account.
Normal response codes: 200
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- keypair_name: keypair_name_path
- user_id: keypair_user
Response
--------
.. rest_parameters:: parameters.yaml
- created_at: created_at
- updated_at: created_at
- fingerprint: keypair_fingerprint
- name: keypair_name
- public_key: keypair_public_key
- user_id: keypair_userid
- type: keypair_type
**Example Show Keypair Details: JSON response**
.. literalinclude:: samples/keypairs/keypairs-get-resp.json
:language: javascript
Delete Keypair
==============
.. rest_method:: DELETE /keypairs/{keypair_name}
Deletes a keypair.
Normal response codes: 204
Error response codes: unauthorized(401), forbidden(403), itemNotFound(404)
Request
-------
.. rest_parameters:: parameters.yaml
- keypair_name: keypair_name_path
- user_id: keypair_user
Response
--------
There is no body content for the response of a successful DELETE query

View File

@ -219,6 +219,71 @@ instance_uuid:
in: body in: body
required: true required: true
type: string type: string
keypair_fingerprint:
in: body
required: true
type: string
description: |
The fingerprint for the keypair.
keypair_name:
in: body
required: true
type: string
description: |
A name for the keypair which will be used to reference it later.
keypair_private_key:
description: |
If you do not provide a public key on create, a new keypair will
be built for you, and the private key will be returned during the
initial create call. Make sure to save this, as there is no way to
get this private key again in the future.
in: body
required: false
type: string
keypair_public_key:
description: |
The keypair public key.
in: body
required: true
type: string
keypair_public_key_in:
description: |
The public ssh key to import. If you omit this value, a keypair is
generated for you.
in: body
required: false
type: string
keypair_type:
in: body
required: true
type: string
description: |
The type of the keypair. Allowed values are ``ssh`` or ``x509``.
keypair_type_in:
in: body
required: false
type: string
description: |
The type of the keypair. Allowed values are ``ssh`` or ``x509``.
keypair_userid:
in: body
required: true
type: string
description: |
The user_id for a keypair.
keypair_userid_in:
in: body
required: false
type: string
description: |
The user_id for a keypair. This allows administrative users to
upload keys for other users than themselves.
keypairs:
in: body
type: array
required: true
description: |
Array of Keypair objects
launched_at: launched_at:
description: | description: |
The date and time when the instance was launched. The date and time The date and time when the instance was launched. The date and time

View File

@ -0,0 +1,19 @@
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCw10TR9arvKrQlleazdUe46FBDaixQa6DArb177nYzKTr/dLnHZo1xjLu9bHNgv8dBSs4fgnMbY1+qugCIfvq7DRnKH4jMGP6g+vf+xELa8BnA/wo+rY4Ei1IOoSOCo0pTMbMh0IZTv4a4GBCWsYBab97xGEytSaH4ysoWdayiOCuYsH7KVWvSZyZgV6SWbprYEJS4mFl7ZH6Yd4zSKMMR11xqYwcuRHf/+0kJV1cFci6mBSMLWha2UO7WS5hwOkSuiveQbGbbemQr1HRwF2xCupp3fB/3RSOjY9tXyODeHdKfWzOxL9T5KWuCuT3n7y5Dsweigya1gMNuo9X1ecXh Generated-by-Mogan",
"user_id": "4c1ea1d7a518420ca5fba507fdb38067",
"name": "test3",
"links": [
{
"href": "http://10.229.40.107:6688/v1/keypairs/test3",
"rel": "self"
},
{
"href": "http://10.229.40.107:6688/keypairs/test3",
"rel": "bookmark"
}
],
"created_at": "2017-04-18T09:02:06+00:00",
"updated_at": null,
"fingerprint": "00:61:8b:1d:18:c6:73:8d:d0:02:75:8b:8e:b7:f7:ae",
"type": "ssh"
}

View File

@ -0,0 +1,4 @@
{
"name": "keypair-d20a3d59-9433-4b79-8726-20b431d89c78",
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova"
}

View File

@ -0,0 +1,19 @@
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova",
"user_id": "4c1ea1d7a518420ca5fba507fdb38067",
"name": "keypair-test",
"links": [
{
"href": "http://10.229.40.107:6688/v1/keypairs/keypair-test",
"rel": "self"
},
{
"href": "http://10.229.40.107:6688/keypairs/keypair-test",
"rel": "bookmark"
}
],
"created_at": "2017-04-18T09:16:18.182631+00:00",
"updated_at": null,
"fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c",
"type": "ssh"
}

View File

@ -0,0 +1,23 @@
{
"keypairs": [
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCw10TR9arvKrQlleazdUe46FBDaixQa6DArb177nYzKTr/dLnHZo1xjLu9bHNgv8dBSs4fgnMbY1+qugCIfvq7DRnKH4jMGP6g+vf+xELa8BnA/wo+rY4Ei1IOoSOCo0pTMbMh0IZTv4a4GBCWsYBab97xGEytSaH4ysoWdayiOCuYsH7KVWvSZyZgV6SWbprYEJS4mFl7ZH6Yd4zSKMMR11xqYwcuRHf/+0kJV1cFci6mBSMLWha2UO7WS5hwOkSuiveQbGbbemQr1HRwF2xCupp3fB/3RSOjY9tXyODeHdKfWzOxL9T5KWuCuT3n7y5Dsweigya1gMNuo9X1ecXh Generated-by-Mogan",
"user_id": "4c1ea1d7a518420ca5fba507fdb38067",
"name": "test3",
"links": [
{
"href": "http://10.229.40.107:6688/v1/keypairs/test3",
"rel": "self"
},
{
"href": "http://10.229.40.107:6688/keypairs/test3",
"rel": "bookmark"
}
],
"created_at": "2017-04-18T09:02:06+00:00",
"updated_at": null,
"fingerprint": "00:61:8b:1d:18:c6:73:8d:d0:02:75:8b:8e:b7:f7:ae",
"type": "ssh"
}
]
}

View File

@ -0,0 +1,5 @@
{
"user_id": "15e4229548a64fa9b1ccb67423144252",
"type": "ssh",
"name": "test2"
}

View File

@ -0,0 +1,20 @@
{
"public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDyCfP9RwNBG6BFvl8sWLRNRAC6QtTmYKflE7XPVAJZxceiouZ+5QbuDa8RPBorzfnlytAgwM/g9KyNvzX5NmH40cwP9h4uKoXZke3dK7UxCDv/ab+1UpONgbGOBN23gj6JRtlOCl7iRxSpgmKxiirbxKkpwFO1MI1dXYE7FHmrHsxy6FA4rKgM+U2aY8tPf80MHkMhO7P9nHf1YjLaHuRHmehKQgIAMUbb527PXW7JE3Q9qPjl5L4fpuP/dtx/hqfj3tEnQHY/8dyDtWLmi1U+7E7D8kbMh0PoMFfKUpwJ1e43MvvnOLuvIz8tTpOnN17pVpM1Yg5a7604Yb8LyOo/ Generated-by-Mogan",
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA8gnz/UcDQRugRb5fLFi0TUQAukLU5mCn5RO1z1QCWcXHoqLm\nfuUG7g2vETwaK8355crQIMDP4PSsjb81+TZh+NHMD/YeLiqF2ZHt3Su1MQg7/2m/\ntVKTjYGxjgTdt4I+iUbZTgpe4kcUqYJisYoq28SpKcBTtTCNXV2BOxR5qx7McuhQ\nOKyoDPlNmmPLT3/NDB5DITuz/Zx39WIy2h7kR5noSkICADFG2+duz11uyRN0Paj4\n5eS+H6bj/3bcf4an497RJ0B2P/Hcg7Vi5otVPuxOw/JGzIdD6DBXylKcCdXuNzL7\n5zi7ryM/LU6Tpzde6VaTNWIOWu+tOGG/C8jqPwIDAQABAoIBAEgZEucFekCwvANK\nfAs3uS2y7gyNz+F4NUsVnfjOa4zWT2tw3vw5uOC8jsOxhZI63w/GZEz9Ym7+M1Bd\n/vPncTOvOvweMktKO2jeAV76oBSlAUpJ8+NNX8qtMXi+llUNpRc7VYvbpvv8dHkx\n77g3EiE46bMYKVc1yUZgjhhNNxjzlVyt3Hq5mo+LfrXKLxsr0WwH3pPivmEQ6USw\neyRGuqDQGdl4KbNg7UyeBMyKJ/YXwk1zzeAc/WSek/tRfEhrxnPwU/2fOKQl6pSS\nPE2XfNCCCH/yh6Wr8w/GO3Ob0S7i4zwQfptC+xfQJ/7P49OwtlHefGNwnpg04RWd\nWex8zEECgYEA/W9ek9IQ3HqA43UdXdebkEqzPEPcO+YLDjjHUzghyk/Hprao7W+M\nVvQMQ1PlUMMqeWayOQw5yQDETc94LTj6LV7enDkHmaAY+IHYXe+ScsLP+G23Hkw9\nn2IrxZI6zI+28+CmrAaCh+e3jLMrfEQbi3yOSxydHC2YZ7b53O7NXXECgYEA9H0O\nnK9VnyD8dpzbkVrmF3aGCE937201OKvtkaaHEaJJPRI4jN+a4iSAYVhJWSlLMbC5\nSFXyAy7vsTPUuhvFpEAKThUt5ULkXlKhmqtxE6+2xhbuqO5kpYzLK7oOdpyA4YqP\nNuMGbOq0ovAoxjHE24wxBzZrK9AV0ZOTRLppqq8CgYB50OdH7CfYojWDn05vRexr\nTcybQg8A55EW0+nTMV7kjLZthszp2708KnAeiJvn1vd6hQdTbnH0EJ9Ku1eLfSCb\nYEdmFe92Q0LdaCQk+ruM1+D5C1uCf6j7DEf33lLO8qFA1hGnDDX/tzw9r/1N7LrE\nsCkBJ47I9Y2VBJlTPaGOsQKBgQCAIvsBi7NoTzWCRPue1vE44tmkiWHmjmoSZamB\naLHpwBB6fY495wOZ+l9+pXLr1ASg6mpxSvooSPU+/ldDo0KWrym3esovGjvuY4hn\nM+tz0egNMf+rciY1zfC93imuaJ/zlVcyARJhCzHZI9164qK2HmejzBWnRMvqp1nL\n75dp6QKBgAmDdeINSwAOCGgB/byq8V4IcOrKxPLQUcwb5VcwQRH1CsyHAOmQv4I1\nv6gw5S7FeorVBwtyj+en6O98P27TCQUv6JxDD0c5Eh6KBjWzLNAxk7hwsHVCNT0k\nDhQ6pYuF8E7jaiHsk6aIcu1ADxIpCoPNqkmhLuL86XMAEuh07jV3\n-----END RSA PRIVATE KEY-----\n",
"user_id": "4c1ea1d7a518420ca5fba507fdb38067",
"name": "test16",
"links": [
{
"href": "http://10.229.40.107:6688/v1/keypairs/test16",
"rel": "self"
},
{
"href": "http://10.229.40.107:6688/keypairs/test16",
"rel": "bookmark"
}
],
"created_at": "2017-04-19T03:01:34.398162+00:00",
"updated_at": null,
"fingerprint": "ab:11:7d:ef:6f:26:76:49:5c:cc:e9:be:6a:55:68:b3",
"type": "ssh"
}

View File

@ -28,6 +28,7 @@ from mogan.api.controllers import link
from mogan.api.controllers.v1 import availability_zone from mogan.api.controllers.v1 import availability_zone
from mogan.api.controllers.v1 import flavors from mogan.api.controllers.v1 import flavors
from mogan.api.controllers.v1 import instances from mogan.api.controllers.v1 import instances
from mogan.api.controllers.v1 import keypairs
from mogan.api import expose from mogan.api import expose
@ -46,6 +47,9 @@ class V1(base.APIBase):
availability_zones = [link.Link] availability_zones = [link.Link]
"""Links to the availability zones resource""" """Links to the availability zones resource"""
keypairs = [link.Link]
"""Links to the keypairs resource"""
@staticmethod @staticmethod
def convert(): def convert():
v1 = V1() v1 = V1()
@ -72,6 +76,14 @@ class V1(base.APIBase):
'availability_zones', '', 'availability_zones', '',
bookmark=True) bookmark=True)
] ]
v1.keypairs = [link.Link.make_link('self',
pecan.request.public_url,
'keypairs', ''),
link.Link.make_link('bookmark',
pecan.request.public_url,
'keypairs', '',
bookmark=True)
]
return v1 return v1
@ -81,6 +93,7 @@ class Controller(rest.RestController):
flavors = flavors.FlavorsController() flavors = flavors.FlavorsController()
instances = instances.InstanceController() instances = instances.InstanceController()
availability_zones = availability_zone.AvailabilityZoneController() availability_zones = availability_zone.AvailabilityZoneController()
keypairs = keypairs.KeyPairController()
@expose.expose(V1) @expose.expose(V1)
def get(self): def get(self):

View File

@ -0,0 +1,165 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
# 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.
import pecan
from pecan import rest
from six.moves import http_client
import wsme
from wsme import types as wtypes
from mogan.api.controllers import base
from mogan.api.controllers import link
from mogan.api.controllers.v1.schemas import keypairs as keypairs_schema
from mogan.api.controllers.v1 import types
from mogan.api import expose
from mogan.api import validation
from mogan import objects
from mogan.objects import keypair as keypair_obj
class KeyPair(base.APIBase):
"""API representation of a keypair.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
a keypair.
"""
user_id = types.uuid
"""The user id of the keypair"""
name = wtypes.text
"""The name of the keypair"""
fingerprint = wtypes.text
"""The fingerprint of the keypair"""
public_key = wtypes.text
"""The public_key of the keypair"""
private_key = wtypes.text
"""The private_key of the keypair"""
type = wtypes.Enum(str, 'ssh', 'x509')
"""The type of the keypair"""
links = wsme.wsattr([link.Link], readonly=True)
"""A list containing a self link"""
def __init__(self, **kwargs):
self.fields = []
if 'private_key' in kwargs:
self.fields.append('private_key')
setattr(self, 'private_key',
kwargs.get('private_key', wtypes.Unset))
for field in objects.KeyPair.fields:
# Skip fields we do not expose.
if not hasattr(self, field):
continue
self.fields.append(field)
setattr(self, field, kwargs.get(field, wtypes.Unset))
@classmethod
def convert_with_links(cls, rpc_keypair):
if hasattr(rpc_keypair, 'private_key') and rpc_keypair.private_key:
keypair = KeyPair(private_key=rpc_keypair.private_key,
**rpc_keypair.as_dict())
else:
keypair = KeyPair(**rpc_keypair.as_dict())
url = pecan.request.public_url
keypair.links = [link.Link.make_link('self', url,
'keypairs',
keypair.name),
link.Link.make_link('bookmark', url,
'keypairs',
keypair.name,
bookmark=True)
]
return keypair
class KeyPairCollection(base.APIBase):
"""API representation of a collection of keypairs."""
keypairs = [KeyPair]
"""A list containing Instance Type objects"""
@staticmethod
def convert_with_links(keypairs, url=None, **kwargs):
collection = KeyPairCollection()
collection.keypairs = [KeyPair.convert_with_links(keypair)
for keypair in keypairs]
return collection
class KeyPairController(rest.RestController):
@expose.expose(KeyPair, body=types.jsontype,
status_code=http_client.CREATED)
def post(self, body):
"""Generate a new keypair or import an existed keypair.
:param body: the request body of keypair creation.
"""
validation.check_schema(body, keypairs_schema.create_keypair)
name = body['name']
user_id = body.get('user_id') or pecan.request.context.user_id
key_type = body.get('type', keypair_obj.KEYPAIR_TYPE_SSH)
if 'public_key' in body:
keypair = pecan.request.engine_api.import_key_pair(
pecan.request.context, user_id, name,
body['public_key'], key_type)
else:
keypair, private_key = pecan.request.engine_api.create_key_pair(
pecan.request.context, user_id, name, key_type)
keypair.private_key = private_key
# Set the HTTP Location Header
pecan.response.location = link.build_url('keypairs',
keypair.name)
return KeyPair.convert_with_links(keypair)
@expose.expose(None, wtypes.text, wtypes.text,
status_code=http_client.NO_CONTENT)
def delete(self, key_name, user_id=None):
"""Delete a keypair.
:param key_name: the name of keypair to be deleted.
"""
# TODO(liusheng): the input user_id should be only suport by admin
# as default, need to add policy check here.
user_id = user_id or pecan.request.context.user_id
pecan.request.engine_api.delete_key_pair(
pecan.request.context, user_id, key_name)
@expose.expose(KeyPair, wtypes.text, wtypes.text)
def get_one(self, key_name, user_id=None):
"""Query one keypair.
:param key_name: the name of keypair to be queried.
"""
# TODO(liusheng): the input user_id should be only suport by admin
# as default, need to add policy check here.
user_id = user_id or pecan.request.context.user_id
keypair = pecan.request.engine_api.get_key_pair(
pecan.request.context, user_id, key_name)
return KeyPair.convert_with_links(keypair)
@expose.expose(KeyPairCollection, wtypes.text)
def get_all(self, user_id=None):
"""Query all keypairs of current user."""
# TODO(liusheng): the input user_id should be only suport by admin
# as default, need to add policy check here.
user_id = user_id or pecan.request.context.user_id
keypairs = pecan.request.engine_api.get_key_pairs(
pecan.request.context, user_id)
return KeyPairCollection.convert_with_links(keypairs)

View File

@ -0,0 +1,32 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
# All Rights Reserved.
#
# 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 mogan.api.validation import parameter_types
create_keypair = {
"type": "object",
"properties": {
'name': parameter_types.name,
'user_id': {'type': 'string'},
'type': {
'type': 'string',
'enum': ['ssh', 'x509']
},
'public_key': {'type': 'string'},
},
'required': ['name'],
'additionalProperties': False,
}

View File

@ -400,4 +400,15 @@ class Base64Exception(MoganException):
_msg_fmt = _("Invalid Base 64 data for file %(path)s") _msg_fmt = _("Invalid Base 64 data for file %(path)s")
class KeyPairExists(MoganException):
_msg_fmt = _("KeyPaire with key name %(key_name)s already exists.")
class KeypairNotFound(NotFound):
_msg_fmt = _("Keypair %(name)s not found for user %(user_id)s")
class InvalidKeypair(Invalid):
_msg_fmt = _("Keypair data is invalid: %(reason)s")
ObjectActionError = obj_exc.ObjectActionError ObjectActionError = obj_exc.ObjectActionError

View File

@ -15,6 +15,8 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
import base64
import binascii
import contextlib import contextlib
import functools import functools
import inspect import inspect
@ -24,18 +26,23 @@ import shutil
import tempfile import tempfile
import traceback import traceback
from cryptography.hazmat import backends
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography import x509
from oslo_concurrency import lockutils from oslo_concurrency import lockutils
from oslo_concurrency import processutils from oslo_concurrency import processutils
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import encodeutils from oslo_utils import encodeutils
import paramiko
import six import six
from mogan.common import exception from mogan.common import exception
from mogan.common.i18n import _
from mogan.common import states from mogan.common import states
from mogan.conf import CONF from mogan.conf import CONF
from mogan import objects from mogan import objects
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
synchronized = lockutils.synchronized_with_prefix('mogan-') synchronized = lockutils.synchronized_with_prefix('mogan-')
@ -283,3 +290,122 @@ def mkfs(fs, path, label=None, run_as_root=False):
def trycmd(*args, **kwargs): def trycmd(*args, **kwargs):
"""Convenience wrapper around oslo's trycmd() method.""" """Convenience wrapper around oslo's trycmd() method."""
return processutils.trycmd(*args, **kwargs) return processutils.trycmd(*args, **kwargs)
def check_string_length(value, name=None, min_length=0, max_length=None):
"""Check the length of specified string
:param value: the value of the string
:param name: the name of the string
:param min_length: the min_length of the string
:param max_length: the max_length of the string
"""
if not isinstance(value, six.string_types):
if name is None:
msg = "The input is not a string or unicode"
else:
msg = "%s is not a string or unicode" % name
raise exception.Invalid(message=msg)
if name is None:
name = value
if len(value) < min_length:
msg = _("%(name)s has a minimum character requirement of "
"%(min_length)s.") % {'name': name, 'min_length': min_length}
raise exception.Invalid(message=msg)
if max_length and len(value) > max_length:
msg = _("%(name)s has more than %(max_length)s "
"characters.") % {'name': name, 'max_length': max_length}
raise exception.Invalid(message=msg)
def _create_x509_openssl_config(conffile, upn):
content = ("distinguished_name = req_distinguished_name\n"
"[req_distinguished_name]\n"
"[v3_req_client]\n"
"extendedKeyUsage = clientAuth\n"
"subjectAltName = otherName:""1.3.6.1.4.1.311.20.2.3;UTF8:%s\n")
with open(conffile, 'w') as file:
file.write(content % upn)
def generate_winrm_x509_cert(user_id, bits=2048):
"""Generate a cert for passwordless auth for user in project."""
subject = '/CN=%s' % user_id
upn = '%s@localhost' % user_id
with tempdir() as tmpdir:
keyfile = os.path.abspath(os.path.join(tmpdir, 'temp.key'))
conffile = os.path.abspath(os.path.join(tmpdir, 'temp.conf'))
_create_x509_openssl_config(conffile, upn)
(certificate, _err) = execute(
'openssl', 'req', '-x509', '-nodes', '-days', '3650',
'-config', conffile, '-newkey', 'rsa:%s' % bits,
'-outform', 'PEM', '-keyout', keyfile, '-subj', subject,
'-extensions', 'v3_req_client',
binary=True)
(out, _err) = execute('openssl', 'pkcs12', '-export',
'-inkey', keyfile, '-password', 'pass:',
process_input=certificate,
binary=True)
private_key = base64.b64encode(out)
fingerprint = generate_x509_fingerprint(certificate)
if six.PY3:
private_key = private_key.decode('ascii')
certificate = certificate.decode('utf-8')
return (private_key, certificate, fingerprint)
def generate_fingerprint(public_key):
try:
pub_bytes = public_key.encode('utf-8')
# Test that the given public_key string is a proper ssh key. The
# returned object is unused since pyca/cryptography does not have a
# fingerprint method.
serialization.load_ssh_public_key(
pub_bytes, backends.default_backend())
pub_data = base64.b64decode(public_key.split(' ')[1])
digest = hashes.Hash(hashes.MD5(), backends.default_backend())
digest.update(pub_data)
md5hash = digest.finalize()
raw_fp = binascii.hexlify(md5hash)
if six.PY3:
raw_fp = raw_fp.decode('ascii')
return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2]))
except Exception:
raise exception.InvalidKeypair(
reason=_('failed to generate fingerprint'))
def generate_x509_fingerprint(pem_key):
try:
if isinstance(pem_key, six.text_type):
pem_key = pem_key.encode('utf-8')
cert = x509.load_pem_x509_certificate(
pem_key, backends.default_backend())
raw_fp = binascii.hexlify(cert.fingerprint(hashes.SHA1()))
if six.PY3:
raw_fp = raw_fp.decode('ascii')
return ':'.join(a + b for a, b in zip(raw_fp[::2], raw_fp[1::2]))
except (ValueError, TypeError, binascii.Error) as ex:
raise exception.InvalidKeypair(
reason=_('failed to generate X509 fingerprint. '
'Error message: %s') % ex)
def generate_key_pair(bits=2048):
key = paramiko.RSAKey.generate(bits)
keyout = six.StringIO()
key.write_private_key(keyout)
private_key = keyout.getvalue()
public_key = '%s %s Generated-by-Mogan' % (
key.get_name(), key.get_base64())
fingerprint = generate_fingerprint(public_key)
return (private_key, public_key, fingerprint)

View File

@ -248,3 +248,28 @@ class Connection(object):
@abc.abstractmethod @abc.abstractmethod
def reservation_expire(self, context): def reservation_expire(self, context):
"""expire all reservations which has been expired""" """expire all reservations which has been expired"""
@abc.abstractmethod
def key_pair_create(self, context, values):
"""Create a key_pair from the values dictionary."""
return IMPL.key_pair_create(context, values)
@abc.abstractmethod
def key_pair_destroy(self, context, user_id, name):
"""Destroy the key_pair or raise if it does not exist."""
return IMPL.key_pair_destroy(context, user_id, name)
@abc.abstractmethod
def key_pair_get(self, context, user_id, name):
"""Get a key_pair or raise if it does not exist."""
return IMPL.key_pair_get(context, user_id, name)
@abc.abstractmethod
def key_pair_get_all_by_user(self, context, user_id):
"""Get all key_pairs by user."""
return IMPL.key_pair_get_all_by_user(context, user_id)
@abc.abstractmethod
def key_pair_count_by_user(self, context, user_id):
"""Count number of key pairs for the given user ID."""
return IMPL.key_pair_count_by_user(context, user_id)

View File

@ -224,3 +224,19 @@ def upgrade():
mysql_ENGINE='InnoDB', mysql_ENGINE='InnoDB',
mysql_DEFAULT_CHARSET='UTF8' mysql_DEFAULT_CHARSET='UTF8'
) )
op.create_table(
'key_pairs',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), primary_key=True, nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('user_id', sa.String(length=255), nullable=True),
sa.Column('fingerprint', sa.String(255)),
sa.Column('public_key', sa.Text()),
sa.Column('type', sa.Enum('ssh', 'x509'), nullable=False,
default='ssh'),
sa.UniqueConstraint('user_id', 'name',
name="uniq_key_pairs0user_id0name"),
mysql_engine='InnoDB',
mysql_charset='utf8'
)

View File

@ -836,6 +836,38 @@ class Connection(api.Connection):
uuid = reservation.uuid uuid = reservation.uuid
raise exception.ReservationNotFound(uuid=uuid) raise exception.ReservationNotFound(uuid=uuid)
def key_pair_create(self, context, values):
key_pair_ref = models.KeyPair()
key_pair_ref.update(values)
with _session_for_write() as session:
try:
session.add(key_pair_ref)
session.flush()
except db_exc.DBDuplicateEntry:
raise exception.KeyPairExists(key_name=values['name'])
return key_pair_ref
def key_pair_destroy(self, context, user_id, name):
result = model_query(context, models.KeyPair).filter_by(
user_id=user_id).filter_by(name=name).delete()
if not result:
raise exception.KeypairNotFound(user_id=user_id, name=name)
def key_pair_get(self, context, user_id, name):
result = model_query(context, models.KeyPair).filter_by(
user_id=user_id).filter_by(name=name).first()
if not result:
raise exception.KeypairNotFound(user_id=user_id, name=name)
return result
def key_pair_get_all_by_user(self, context, user_id):
query = model_query(context, models.KeyPair).filter_by(user_id=user_id)
return query.all()
def key_pair_count_by_user(self, context, user_id):
return model_query(context, models.KeyPair).filter_by(
user_id=user_id).count()
def _type_get_id_from_type_query(context, type_id): def _type_get_id_from_type_query(context, type_id):
return model_query(context, models.InstanceTypes). \ return model_query(context, models.InstanceTypes). \

View File

@ -312,3 +312,22 @@ class Reservation(Base):
quota = orm.relationship( quota = orm.relationship(
"Quota", foreign_keys=allocated_id, "Quota", foreign_keys=allocated_id,
primaryjoin='Reservation.allocated_id == Quota.id') primaryjoin='Reservation.allocated_id == Quota.id')
class KeyPair(Base):
"""Represents a public key pair for ssh / WinRM."""
__tablename__ = 'key_pairs'
__table_args__ = (
schema.UniqueConstraint("user_id", "name",
name="uniq_key_pairs0user_id0name"),
)
id = Column(Integer, primary_key=True, nullable=False)
name = Column(String(255), nullable=False)
user_id = Column(String(255), nullable=False)
fingerprint = Column(String(255))
public_key = Column(Text())
type = Column(Enum('ssh', 'x509', name='keypair_types'),
nullable=False, server_default='ssh')

View File

@ -14,6 +14,7 @@
# under the License. # under the License.
"""Handles all requests relating to compute resources""" """Handles all requests relating to compute resources"""
import string
import base64 import base64
import binascii import binascii
@ -33,6 +34,7 @@ from mogan.engine import rpcapi
from mogan import image from mogan import image
from mogan import network from mogan import network
from mogan import objects from mogan import objects
from mogan.objects import keypair as keypair_obj
from mogan.objects import quota from mogan.objects import quota
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -422,3 +424,79 @@ class API(object):
access_url=connect_info['access_url']) access_url=connect_info['access_url'])
return {'url': connect_info['access_url']} return {'url': connect_info['access_url']}
def _validate_new_key_pair(self, context, user_id, key_name, key_type):
safe_chars = "_- " + string.digits + string.ascii_letters
clean_value = "".join(x for x in key_name if x in safe_chars)
if clean_value != key_name:
raise exception.InvalidKeypair(
reason="Keypair name contains unsafe characters")
try:
utils.check_string_length(key_name, min_length=1, max_length=255)
except exception.Invalid:
raise exception.InvalidKeypair(
reason='Keypair name must be string and between '
'1 and 255 characters long')
# TODO(liusheng) add quota check
# count = objects.Quotas.count(context, 'key_pairs', user_id)
#
# try:
# objects.Quotas.limit_check(context, key_pairs=count + 1)
# except exception.OverQuota:
# raise exception.KeypairLimitExceeded()
def import_key_pair(self, context, user_id, key_name, public_key,
key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Import a key pair using an existing public key."""
self._validate_new_key_pair(context, user_id, key_name, key_type)
fingerprint = self._generate_fingerprint(public_key, key_type)
keypair = objects.KeyPair(context)
keypair.user_id = user_id
keypair.name = key_name
keypair.type = key_type
keypair.fingerprint = fingerprint
keypair.public_key = public_key
keypair.create()
return keypair
def create_key_pair(self, context, user_id, key_name,
key_type=keypair_obj.KEYPAIR_TYPE_SSH):
"""Create a new key pair."""
self._validate_new_key_pair(context, user_id, key_name, key_type)
private_key, public_key, fingerprint = self._generate_key_pair(
user_id, key_type)
keypair = objects.KeyPair(context)
keypair.user_id = user_id
keypair.name = key_name
keypair.type = key_type
keypair.fingerprint = fingerprint
keypair.public_key = public_key
keypair.create()
return keypair, private_key
def _generate_fingerprint(self, public_key, key_type):
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
return utils.generate_fingerprint(public_key)
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
return utils.generate_x509_fingerprint(public_key)
def _generate_key_pair(self, user_id, key_type):
if key_type == keypair_obj.KEYPAIR_TYPE_SSH:
return utils.generate_key_pair()
elif key_type == keypair_obj.KEYPAIR_TYPE_X509:
return utils.generate_winrm_x509_cert(user_id)
def delete_key_pair(self, context, user_id, key_name):
"""Delete a keypair by name."""
objects.KeyPair.destroy_by_name(context, user_id, key_name)
def get_key_pairs(self, context, user_id):
"""List key pairs."""
return objects.KeyPairList.get_by_user(context, user_id)
def get_key_pair(self, context, user_id, key_name):
"""Get a keypair by name."""
return objects.KeyPair.get_by_name(context, user_id, key_name)

View File

@ -32,3 +32,4 @@ def register_all():
__import__('mogan.objects.compute_node') __import__('mogan.objects.compute_node')
__import__('mogan.objects.compute_port') __import__('mogan.objects.compute_port')
__import__('mogan.objects.compute_disk') __import__('mogan.objects.compute_disk')
__import__('mogan.objects.keypair')

103
mogan/objects/keypair.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright 2017 Huawei Technologies Co.,LTD.
#
# 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 oslo_versionedobjects import base as object_base
from mogan.common import exception
from mogan.db import api as dbapi
from mogan import objects
from mogan.objects import base
from mogan.objects import fields
KEYPAIR_TYPE_SSH = 'ssh'
KEYPAIR_TYPE_X509 = 'x509'
@base.MoganObjectRegistry.register
class KeyPair(base.MoganObject):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'id': fields.IntegerField(),
'name': fields.StringField(nullable=False),
'user_id': fields.StringField(nullable=True),
'fingerprint': fields.StringField(nullable=True),
'public_key': fields.StringField(nullable=True),
'type': fields.StringField(nullable=False),
}
@staticmethod
def _from_db_object(context, keypair, db_keypair):
ignore = {'deleted': False,
'deleted_at': None}
for key in keypair.fields:
if key in ignore and not hasattr(db_keypair, key):
setattr(keypair, key, ignore[key])
else:
setattr(keypair, key, db_keypair[key])
keypair._context = context
keypair.obj_reset_changes()
return keypair
@classmethod
def get_by_name(cls, context, user_id, name):
db_keypair = cls.dbapi.key_pair_get(context, user_id, name)
return cls._from_db_object(context, cls(), db_keypair)
@classmethod
def destroy_by_name(cls, context, user_id, name):
cls.dbapi.key_pair_destroy(context, user_id, name)
def create(self):
if self.obj_attr_is_set('id'):
raise exception.ObjectActionError(action='create',
reason='already created')
try:
self.dbapi.key_pair_get(self._context, self.user_id, self.name)
raise exception.KeyPairExists(key_name=self.name)
except exception.KeypairNotFound:
pass
updates = self.obj_get_changes()
db_keypair = self.dbapi.key_pair_create(self._context, updates)
self._from_db_object(self._context, self, db_keypair)
def destroy(self):
self.dbapi.key_pair_destroy(self._context, self.user_id, self.name)
@base.MoganObjectRegistry.register
class KeyPairList(object_base.ObjectListBase, base.MoganObject):
# Version 1.0: Initial version
VERSION = '1.0'
dbapi = dbapi.get_instance()
fields = {
'objects': fields.ListOfObjectsField('KeyPair'),
}
@classmethod
def get_count_from_db(cls, context, user_id):
return cls.dbapi.key_pair_count_by_user(context, user_id)
@classmethod
def get_by_user(cls, context, user_id):
db_keypairs = cls.dbapi.key_pair_get_all_by_user(context, user_id)
return object_base.obj_make_list(
context, cls(context), objects.KeyPair, db_keypairs)

View File

@ -0,0 +1,83 @@
#
# 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.
import copy
import mock
from oslo_context import context
from mogan.common import exception
from mogan import objects
from mogan.tests.unit.db import base
class TestKeyPairObject(base.DbTestCase):
def setUp(self):
super(TestKeyPairObject, self).setUp()
self.ctxt = context.get_admin_context()
self.fake_keypair = {
"id": 1,
"public_key": "fake-publick-key",
"user_id": "e78b60069fc9467e97fb4b74de9cadc1",
"name": "test_key",
"fingerprint": "f1:83:34:02:f9:63:79:d4:bd:2a:1d:50:16:61:1b:cc",
"type": "ssh",
"created_at": "2017-04-18T09:16:18.182631+00:00",
"updated_at": None
}
def test_get(self):
with mock.patch.object(self.dbapi, 'key_pair_get',
autospec=True) as mock_keypair_get:
mock_keypair_get.return_value = self.fake_keypair
keypair = objects.KeyPair.get_by_name(
self.context, self.fake_keypair['user_id'],
self.fake_keypair['name'])
mock_keypair_get.assert_called_once_with(
self.context, self.fake_keypair['user_id'],
self.fake_keypair['name'])
self.assertEqual(self.context, keypair._context)
self.assertEqual(self.fake_keypair['name'], keypair.name)
self.assertEqual(self.fake_keypair['user_id'], keypair.user_id)
def test_create(self):
with mock.patch.object(self.dbapi, 'key_pair_get',
autospec=True) as mock_keypair_get:
with mock.patch.object(self.dbapi, 'key_pair_create',
autospec=True) as mock_keypair_create:
mock_keypair_get.side_effect = exception.KeypairNotFound(
user_id=self.fake_keypair['user_id'],
name=self.fake_keypair['name'])
mock_keypair_create.return_value = self.fake_keypair
create_params = copy.copy(self.fake_keypair)
create_params.pop('id')
keypair = objects.KeyPair(self.context,
**create_params)
values = keypair.obj_get_changes()
keypair.create()
mock_keypair_create.assert_called_once_with(
self.context, values)
self.assertEqual(self.fake_keypair['name'], keypair.name)
self.assertEqual(self.fake_keypair['user_id'], keypair.user_id)
def test_destroy(self):
with mock.patch.object(self.dbapi, 'key_pair_destroy',
autospec=True) as mock_keypair_destroy:
mock_keypair_destroy.return_value = self.fake_keypair
keypair = objects.KeyPair(self.context,
**self.fake_keypair)
keypair.destroy()
mock_keypair_destroy.assert_called_once_with(
self.context, self.fake_keypair['user_id'],
self.fake_keypair['name'])

View File

@ -396,6 +396,8 @@ expected_object_fingerprints = {
'InstanceNic': '1.0-78744332fe105f9c1796dc5295713d9f', 'InstanceNic': '1.0-78744332fe105f9c1796dc5295713d9f',
'InstanceNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244', 'InstanceNics': '1.0-33a2e1bb91ad4082f9f63429b77c1244',
'Quota': '1.0-c8caa082f4d726cb63fdc5943f7cd186', 'Quota': '1.0-c8caa082f4d726cb63fdc5943f7cd186',
'KeyPair': '1.0-c6820166e307676c5900f7801831b84c',
'KeyPairList': '1.0-33a2e1bb91ad4082f9f63429b77c1244'
} }

View File

@ -7,6 +7,7 @@ SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
alembic>=0.8.10 # MIT alembic>=0.8.10 # MIT
eventlet!=0.18.3,>=0.18.2 # MIT eventlet!=0.18.3,>=0.18.2 # MIT
WebOb>=1.7.1 # MIT WebOb>=1.7.1 # MIT
cryptography>=1.6 # BSD/Apache-2.0
python-ironicclient>=1.11.0 # Apache-2.0 python-ironicclient>=1.11.0 # Apache-2.0
python-neutronclient>=5.1.0 # Apache-2.0 python-neutronclient>=5.1.0 # Apache-2.0
python-glanceclient>=2.5.0 # Apache-2.0 python-glanceclient>=2.5.0 # Apache-2.0