Skip to content

Commit dee22d8

Browse files
committed
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
1 parent 69b7b9b commit dee22d8

5 files changed

Lines changed: 94 additions & 4 deletions

File tree

doc/source/command-objects/keypair.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,18 @@ Create new public or private key for server ssh access
1818
.. code:: bash
1919
2020
openstack keypair create
21-
[--public-key <file>]
21+
[--public-key <file> | --private-key <file>]
2222
<name>
2323
2424
.. option:: --public-key <file>
2525

2626
Filename for public key to add. If not used, creates a private key.
2727

28+
.. option:: --private-key <file>
29+
30+
Filename for private key to save. If not used, print private key in
31+
console.
32+
2833
.. describe:: <name>
2934

3035
New public or private key name

openstackclient/compute/v2/keypair.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,19 @@ def get_parser(self, prog_name):
4141
metavar='<name>',
4242
help=_("New public or private key name")
4343
)
44-
parser.add_argument(
44+
key_group = parser.add_mutually_exclusive_group()
45+
key_group.add_argument(
4546
'--public-key',
4647
metavar='<file>',
4748
help=_("Filename for public key to add. If not used, "
4849
"creates a private key.")
4950
)
51+
key_group.add_argument(
52+
'--private-key',
53+
metavar='<file>',
54+
help=_("Filename for private key to save. If not used, "
55+
"print private key in console.")
56+
)
5057
return parser
5158

5259
def take_action(self, parsed_args):
@@ -69,13 +76,31 @@ def take_action(self, parsed_args):
6976
public_key=public_key,
7077
)
7178

79+
private_key = parsed_args.private_key
80+
# Save private key into specified file
81+
if private_key:
82+
try:
83+
with io.open(
84+
os.path.expanduser(parsed_args.private_key), 'w+'
85+
) as p:
86+
p.write(keypair.private_key)
87+
except IOError as e:
88+
msg = _("Key file %(private_key)s can not be saved: "
89+
"%(exception)s")
90+
raise exceptions.CommandError(
91+
msg % {"private_key": parsed_args.private_key,
92+
"exception": e}
93+
)
7294
# NOTE(dtroyer): how do we want to handle the display of the private
7395
# key when it needs to be communicated back to the user
7496
# For now, duplicate nova keypair-add command output
7597
info = {}
76-
if public_key:
98+
if public_key or private_key:
7799
info.update(keypair._info)
78-
del info['public_key']
100+
if 'public_key' in info:
101+
del info['public_key']
102+
if 'private_key' in info:
103+
del info['private_key']
79104
return zip(*sorted(six.iteritems(info)))
80105
else:
81106
sys.stdout.write(keypair.private_key)

openstackclient/tests/functional/compute/v2/test_keypair.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import json
1314
import tempfile
1415

1516
from openstackclient.tests.functional import base
@@ -100,6 +101,26 @@ def test_keypair_create_public_key(self):
100101
)
101102
self.assertIn('tmpkey', raw_output)
102103

104+
def test_keypair_create_private_key(self):
105+
"""Test for create keypair with --private-key option.
106+
107+
Test steps:
108+
1) Create keypair with private key file
109+
2) Delete keypair
110+
"""
111+
with tempfile.NamedTemporaryFile() as f:
112+
cmd_output = json.loads(self.openstack(
113+
'keypair create -f json --private-key %s tmpkey' % f.name,
114+
))
115+
self.addCleanup(self.openstack, 'keypair delete tmpkey')
116+
self.assertEqual('tmpkey', cmd_output.get('name'))
117+
self.assertIsNotNone(cmd_output.get('user_id'))
118+
self.assertIsNotNone(cmd_output.get('fingerprint'))
119+
pk_content = f.read()
120+
self.assertInOutput('-----BEGIN RSA PRIVATE KEY-----', pk_content)
121+
self.assertRegex(pk_content, "[0-9A-Za-z+/]+[=]{0,3}\n")
122+
self.assertInOutput('-----END RSA PRIVATE KEY-----', pk_content)
123+
103124
def test_keypair_create(self):
104125
"""Test keypair create command.
105126

openstackclient/tests/unit/compute/v2/test_keypair.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import mock
1717
from mock import call
18+
import uuid
1819

1920
from osc_lib import exceptions
2021
from osc_lib import utils
@@ -115,6 +116,36 @@ def test_keypair_create_public_key(self):
115116
self.assertEqual(self.columns, columns)
116117
self.assertEqual(self.data, data)
117118

119+
def test_keypair_create_private_key(self):
120+
tmp_pk_file = '/tmp/kp-file-' + uuid.uuid4().hex
121+
arglist = [
122+
'--private-key', tmp_pk_file,
123+
self.keypair.name,
124+
]
125+
verifylist = [
126+
('private_key', tmp_pk_file),
127+
('name', self.keypair.name)
128+
]
129+
130+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
131+
132+
with mock.patch('io.open') as mock_open:
133+
mock_open.return_value = mock.MagicMock()
134+
m_file = mock_open.return_value.__enter__.return_value
135+
136+
columns, data = self.cmd.take_action(parsed_args)
137+
138+
self.keypairs_mock.create.assert_called_with(
139+
self.keypair.name,
140+
public_key=None
141+
)
142+
143+
mock_open.assert_called_once_with(tmp_pk_file, 'w+')
144+
m_file.write.assert_called_once_with(self.keypair.private_key)
145+
146+
self.assertEqual(self.columns, columns)
147+
self.assertEqual(self.data, data)
148+
118149

119150
class TestKeypairDelete(TestKeypair):
120151

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
features:
3+
- |
4+
Add ``--private-key`` option for ``keypair create`` command to specify the
5+
private key file to save when a keypair is created, removing the need to
6+
copy the output and paste it into a new file. This is a convenient way
7+
to save private key in OSC interactive mode.
8+
[Bug `1549410 <https://bugs.launchpad.net/python-openstackclient/+bug/1549410>`_]

0 commit comments

Comments
 (0)