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:
Rui Chen 2017-02-27 14:35:05 +08:00
parent 69b7b9b059
commit dee22d8faa
5 changed files with 94 additions and 4 deletions

View File

@ -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

View File

@ -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)
del info['public_key'] if 'public_key' in info:
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)

View File

@ -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.

View File

@ -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):

View 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>`_]