Add "--private-key" option for "keypair create"
Aim to specify the private key file to save when keypair is created. That is a convenient way to save private key in OSC interactive mode, avoid to copy CLI output, then paste it into file. Change-Id: I119d2f2a3323d17ecbe3de4e27f35e1ceef6e0a5 Closes-Bug: #1549410
This commit is contained in:
parent
69b7b9b059
commit
dee22d8faa
@ -18,13 +18,18 @@ Create new public or private key for server ssh access
|
|||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
openstack keypair create
|
openstack keypair create
|
||||||
[--public-key <file>]
|
[--public-key <file> | --private-key <file>]
|
||||||
<name>
|
<name>
|
||||||
|
|
||||||
.. option:: --public-key <file>
|
.. option:: --public-key <file>
|
||||||
|
|
||||||
Filename for public key to add. If not used, creates a private key.
|
Filename for public key to add. If not used, creates a private key.
|
||||||
|
|
||||||
|
.. option:: --private-key <file>
|
||||||
|
|
||||||
|
Filename for private key to save. If not used, print private key in
|
||||||
|
console.
|
||||||
|
|
||||||
.. describe:: <name>
|
.. describe:: <name>
|
||||||
|
|
||||||
New public or private key name
|
New public or private key name
|
||||||
|
@ -41,12 +41,19 @@ class CreateKeypair(command.ShowOne):
|
|||||||
metavar='<name>',
|
metavar='<name>',
|
||||||
help=_("New public or private key name")
|
help=_("New public or private key name")
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
key_group = parser.add_mutually_exclusive_group()
|
||||||
|
key_group.add_argument(
|
||||||
'--public-key',
|
'--public-key',
|
||||||
metavar='<file>',
|
metavar='<file>',
|
||||||
help=_("Filename for public key to add. If not used, "
|
help=_("Filename for public key to add. If not used, "
|
||||||
"creates a private key.")
|
"creates a private key.")
|
||||||
)
|
)
|
||||||
|
key_group.add_argument(
|
||||||
|
'--private-key',
|
||||||
|
metavar='<file>',
|
||||||
|
help=_("Filename for private key to save. If not used, "
|
||||||
|
"print private key in console.")
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
@ -69,13 +76,31 @@ class CreateKeypair(command.ShowOne):
|
|||||||
public_key=public_key,
|
public_key=public_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private_key = parsed_args.private_key
|
||||||
|
# Save private key into specified file
|
||||||
|
if private_key:
|
||||||
|
try:
|
||||||
|
with io.open(
|
||||||
|
os.path.expanduser(parsed_args.private_key), 'w+'
|
||||||
|
) as p:
|
||||||
|
p.write(keypair.private_key)
|
||||||
|
except IOError as e:
|
||||||
|
msg = _("Key file %(private_key)s can not be saved: "
|
||||||
|
"%(exception)s")
|
||||||
|
raise exceptions.CommandError(
|
||||||
|
msg % {"private_key": parsed_args.private_key,
|
||||||
|
"exception": e}
|
||||||
|
)
|
||||||
# NOTE(dtroyer): how do we want to handle the display of the private
|
# NOTE(dtroyer): how do we want to handle the display of the private
|
||||||
# key when it needs to be communicated back to the user
|
# key when it needs to be communicated back to the user
|
||||||
# For now, duplicate nova keypair-add command output
|
# For now, duplicate nova keypair-add command output
|
||||||
info = {}
|
info = {}
|
||||||
if public_key:
|
if public_key or private_key:
|
||||||
info.update(keypair._info)
|
info.update(keypair._info)
|
||||||
|
if 'public_key' in info:
|
||||||
del info['public_key']
|
del info['public_key']
|
||||||
|
if 'private_key' in info:
|
||||||
|
del info['private_key']
|
||||||
return zip(*sorted(six.iteritems(info)))
|
return zip(*sorted(six.iteritems(info)))
|
||||||
else:
|
else:
|
||||||
sys.stdout.write(keypair.private_key)
|
sys.stdout.write(keypair.private_key)
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from openstackclient.tests.functional import base
|
from openstackclient.tests.functional import base
|
||||||
@ -100,6 +101,26 @@ class KeypairTests(KeypairBase):
|
|||||||
)
|
)
|
||||||
self.assertIn('tmpkey', raw_output)
|
self.assertIn('tmpkey', raw_output)
|
||||||
|
|
||||||
|
def test_keypair_create_private_key(self):
|
||||||
|
"""Test for create keypair with --private-key option.
|
||||||
|
|
||||||
|
Test steps:
|
||||||
|
1) Create keypair with private key file
|
||||||
|
2) Delete keypair
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
cmd_output = json.loads(self.openstack(
|
||||||
|
'keypair create -f json --private-key %s tmpkey' % f.name,
|
||||||
|
))
|
||||||
|
self.addCleanup(self.openstack, 'keypair delete tmpkey')
|
||||||
|
self.assertEqual('tmpkey', cmd_output.get('name'))
|
||||||
|
self.assertIsNotNone(cmd_output.get('user_id'))
|
||||||
|
self.assertIsNotNone(cmd_output.get('fingerprint'))
|
||||||
|
pk_content = f.read()
|
||||||
|
self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', pk_content)
|
||||||
|
self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n")
|
||||||
|
self.assertInOutput('-----END RSA PRIVATE KEY-----', pk_content)
|
||||||
|
|
||||||
def test_keypair_create(self):
|
def test_keypair_create(self):
|
||||||
"""Test keypair create command.
|
"""Test keypair create command.
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import mock
|
import mock
|
||||||
from mock import call
|
from mock import call
|
||||||
|
import uuid
|
||||||
|
|
||||||
from osc_lib import exceptions
|
from osc_lib import exceptions
|
||||||
from osc_lib import utils
|
from osc_lib import utils
|
||||||
@ -115,6 +116,36 @@ class TestKeypairCreate(TestKeypair):
|
|||||||
self.assertEqual(self.columns, columns)
|
self.assertEqual(self.columns, columns)
|
||||||
self.assertEqual(self.data, data)
|
self.assertEqual(self.data, data)
|
||||||
|
|
||||||
|
def test_keypair_create_private_key(self):
|
||||||
|
tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex
|
||||||
|
arglist = [
|
||||||
|
'--private-key', tmp_pk_file,
|
||||||
|
self.keypair.name,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('private_key', tmp_pk_file),
|
||||||
|
('name', self.keypair.name)
|
||||||
|
]
|
||||||
|
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
with mock.patch('io.open') as mock_open:
|
||||||
|
mock_open.return_value = mock.MagicMock()
|
||||||
|
m_file = mock_open.return_value.__enter__.return_value
|
||||||
|
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.keypairs_mock.create.assert_called_with(
|
||||||
|
self.keypair.name,
|
||||||
|
public_key=None
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
|
||||||
|
m_file.write.assert_called_once_with(self.keypair.private_key)
|
||||||
|
|
||||||
|
self.assertEqual(self.columns, columns)
|
||||||
|
self.assertEqual(self.data, data)
|
||||||
|
|
||||||
|
|
||||||
class TestKeypairDelete(TestKeypair):
|
class TestKeypairDelete(TestKeypair):
|
||||||
|
|
||||||
|
8
releasenotes/notes/bug-1549410-8df3a4b12fe13ffa.yaml
Normal file
8
releasenotes/notes/bug-1549410-8df3a4b12fe13ffa.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add ``--private-key`` option for ``keypair create`` command to specify the
|
||||||
|
private key file to save when a keypair is created, removing the need to
|
||||||
|
copy the output and paste it into a new file. This is a convenient way
|
||||||
|
to save private key in OSC interactive mode.
|
||||||
|
[Bug `1549410 <https://bugs.launchpad.net/python-openstackclient/+bug/1549410>`_]
|
Loading…
Reference in New Issue
Block a user