Merge "Add keypair support"
This commit is contained in:
commit
0d00ed2fa8
155
api-ref/source/v1/keypairs.inc
Normal file
155
api-ref/source/v1/keypairs.inc
Normal 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
|
@ -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
|
||||||
|
19
api-ref/source/v1/samples/keypairs/keypair-get-resp.json
Normal file
19
api-ref/source/v1/samples/keypairs/keypair-get-resp.json
Normal 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"
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
23
api-ref/source/v1/samples/keypairs/keypair-list-resp.json
Normal file
23
api-ref/source/v1/samples/keypairs/keypair-list-resp.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5
api-ref/source/v1/samples/keypairs/keypair-post-req.json
Normal file
5
api-ref/source/v1/samples/keypairs/keypair-post-req.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"user_id": "15e4229548a64fa9b1ccb67423144252",
|
||||||
|
"type": "ssh",
|
||||||
|
"name": "test2"
|
||||||
|
}
|
20
api-ref/source/v1/samples/keypairs/keypair-post-resp.json
Normal file
20
api-ref/source/v1/samples/keypairs/keypair-post-resp.json
Normal 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"
|
||||||
|
}
|
@ -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):
|
||||||
|
165
mogan/api/controllers/v1/keypairs.py
Normal file
165
mogan/api/controllers/v1/keypairs.py
Normal 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)
|
32
mogan/api/controllers/v1/schemas/keypairs.py
Normal file
32
mogan/api/controllers/v1/schemas/keypairs.py
Normal 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,
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
)
|
||||||
|
@ -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). \
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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
103
mogan/objects/keypair.py
Normal 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)
|
83
mogan/tests/unit/objects/test_keypairs.py
Normal file
83
mogan/tests/unit/objects/test_keypairs.py
Normal 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'])
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user