Repository: softlayer/softlayer-python
Pull request #1951 (2023-05-16 14:16:09)

New Command: `slcli security`
Issue: https://github.com/softlayer/softlayer-python/issues/1942
Observations: `sshkeys` and `ssl` features were merged into `security` feature, and core test were updated to work with commands names that start with other command names (securitygroup and security), before this upgrade if exists a command that starts with other command name this test fails 
```
slcli security -h
Usage: slcli security [OPTIONS] COMMAND [ARGS] ...

        SSH Keys and SSL Certificates.

┌────┬────────┬─────────────────────────────┐
│ -h │ --help │ Show this message and exit. │
└────┴────────┴─────────────────────────────┘

Commands:
  cert-add       Add and upload SSL certificate details.
  cert-download  Download SSL certificate and key file.
  cert-edit      Edit SSL certificate.
  cert-list      List SSL certificates.
  cert-remove    Remove SSL certificate.
  sshkey-add     Add a new SSH key.
  sshkey-edit    Edits an SSH key.
  sshkey-list    List SSH keys.
  sshkey-print   Prints out an SSH key to the screen.
  sshkey-remove  Permanently removes an SSH key.
```


Patch:
diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py
index 79964efb..3d33fbc9 100644
--- a/SoftLayer/CLI/environment.py
+++ b/SoftLayer/CLI/environment.py
@@ -138,7 +138,8 @@ def list_commands(self, *path):
 
                 # offset is used to exclude the path that the caller requested.
                 offset = len(path_str) + 1 if path_str else 0
-                commands.append(command[offset:])
+                if ':' not in command[offset:]:
+                    commands.append(command[offset:])
 
         return sorted(commands)
 
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 3922541f..c2801106 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -332,18 +332,30 @@
     ('securitygroup:event-log', 'SoftLayer.CLI.securitygroup.event_log:get_by_request_id'),
 
     ('sshkey', 'SoftLayer.CLI.sshkey'),
-    ('sshkey:add', 'SoftLayer.CLI.sshkey.add:cli'),
-    ('sshkey:remove', 'SoftLayer.CLI.sshkey.remove:cli'),
-    ('sshkey:edit', 'SoftLayer.CLI.sshkey.edit:cli'),
-    ('sshkey:list', 'SoftLayer.CLI.sshkey.list:cli'),
-    ('sshkey:print', 'SoftLayer.CLI.sshkey.print:cli'),
+    ('sshkey:add', 'SoftLayer.CLI.security.sshkey_add:cli'),
+    ('sshkey:remove', 'SoftLayer.CLI.security.sshkey_remove:cli'),
+    ('sshkey:edit', 'SoftLayer.CLI.security.sshkey_edit:cli'),
+    ('sshkey:list', 'SoftLayer.CLI.security.sshkey_list:cli'),
+    ('sshkey:print', 'SoftLayer.CLI.security.sshkey_print:cli'),
 
     ('ssl', 'SoftLayer.CLI.ssl'),
-    ('ssl:add', 'SoftLayer.CLI.ssl.add:cli'),
-    ('ssl:download', 'SoftLayer.CLI.ssl.download:cli'),
-    ('ssl:edit', 'SoftLayer.CLI.ssl.edit:cli'),
-    ('ssl:list', 'SoftLayer.CLI.ssl.list:cli'),
-    ('ssl:remove', 'SoftLayer.CLI.ssl.remove:cli'),
+    ('ssl:add', 'SoftLayer.CLI.security.cert_add:cli'),
+    ('ssl:download', 'SoftLayer.CLI.security.cert_download:cli'),
+    ('ssl:edit', 'SoftLayer.CLI.security.cert_edit:cli'),
+    ('ssl:list', 'SoftLayer.CLI.security.cert_list:cli'),
+    ('ssl:remove', 'SoftLayer.CLI.security.cert_remove:cli'),
+
+    ('security', 'SoftLayer.CLI.security'),
+    ('security:sshkey-add', 'SoftLayer.CLI.security.sshkey_add:cli'),
+    ('security:sshkey-remove', 'SoftLayer.CLI.security.sshkey_remove:cli'),
+    ('security:sshkey-edit', 'SoftLayer.CLI.security.sshkey_edit:cli'),
+    ('security:sshkey-list', 'SoftLayer.CLI.security.sshkey_list:cli'),
+    ('security:sshkey-print', 'SoftLayer.CLI.security.sshkey_print:cli'),
+    ('security:cert-add', 'SoftLayer.CLI.security.cert_add:cli'),
+    ('security:cert-download', 'SoftLayer.CLI.security.cert_download:cli'),
+    ('security:cert-edit', 'SoftLayer.CLI.security.cert_edit:cli'),
+    ('security:cert-list', 'SoftLayer.CLI.security.cert_list:cli'),
+    ('security:cert-remove', 'SoftLayer.CLI.security.cert_remove:cli'),
 
     ('subnet', 'SoftLayer.CLI.subnet'),
     ('subnet:cancel', 'SoftLayer.CLI.subnet.cancel:cli'),
diff --git a/SoftLayer/CLI/security/__init__.py b/SoftLayer/CLI/security/__init__.py
new file mode 100644
index 00000000..3e7c8325
--- /dev/null
+++ b/SoftLayer/CLI/security/__init__.py
@@ -0,0 +1,2 @@
+"""SSH Keys and SSL Certificates."""
+# :license: MIT, see LICENSE for more details.
diff --git a/SoftLayer/CLI/ssl/add.py b/SoftLayer/CLI/security/cert_add.py
similarity index 100%
rename from SoftLayer/CLI/ssl/add.py
rename to SoftLayer/CLI/security/cert_add.py
diff --git a/SoftLayer/CLI/ssl/download.py b/SoftLayer/CLI/security/cert_download.py
similarity index 100%
rename from SoftLayer/CLI/ssl/download.py
rename to SoftLayer/CLI/security/cert_download.py
diff --git a/SoftLayer/CLI/ssl/edit.py b/SoftLayer/CLI/security/cert_edit.py
similarity index 100%
rename from SoftLayer/CLI/ssl/edit.py
rename to SoftLayer/CLI/security/cert_edit.py
diff --git a/SoftLayer/CLI/ssl/list.py b/SoftLayer/CLI/security/cert_list.py
similarity index 100%
rename from SoftLayer/CLI/ssl/list.py
rename to SoftLayer/CLI/security/cert_list.py
diff --git a/SoftLayer/CLI/ssl/remove.py b/SoftLayer/CLI/security/cert_remove.py
similarity index 100%
rename from SoftLayer/CLI/ssl/remove.py
rename to SoftLayer/CLI/security/cert_remove.py
diff --git a/SoftLayer/CLI/sshkey/add.py b/SoftLayer/CLI/security/sshkey_add.py
similarity index 100%
rename from SoftLayer/CLI/sshkey/add.py
rename to SoftLayer/CLI/security/sshkey_add.py
diff --git a/SoftLayer/CLI/sshkey/edit.py b/SoftLayer/CLI/security/sshkey_edit.py
similarity index 100%
rename from SoftLayer/CLI/sshkey/edit.py
rename to SoftLayer/CLI/security/sshkey_edit.py
diff --git a/SoftLayer/CLI/sshkey/list.py b/SoftLayer/CLI/security/sshkey_list.py
similarity index 100%
rename from SoftLayer/CLI/sshkey/list.py
rename to SoftLayer/CLI/security/sshkey_list.py
diff --git a/SoftLayer/CLI/sshkey/print.py b/SoftLayer/CLI/security/sshkey_print.py
similarity index 100%
rename from SoftLayer/CLI/sshkey/print.py
rename to SoftLayer/CLI/security/sshkey_print.py
diff --git a/SoftLayer/CLI/sshkey/remove.py b/SoftLayer/CLI/security/sshkey_remove.py
similarity index 100%
rename from SoftLayer/CLI/sshkey/remove.py
rename to SoftLayer/CLI/security/sshkey_remove.py
diff --git a/docs/cli/security.rst b/docs/cli/security.rst
new file mode 100644
index 00000000..2450110d
--- /dev/null
+++ b/docs/cli/security.rst
@@ -0,0 +1,84 @@
+.. _cli_security:
+
+SSH Keys and SSL Certificates
+========
+
+.. click:: SoftLayer.CLI.security.sshkey_add:cli
+    :prog: security sshkey-add
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_add:cli
+    :prog: sshkey add
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_remove:cli
+    :prog: security sshkey-remove
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_remove:cli
+    :prog: sshkey remove
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_edit:cli
+    :prog: security sshkey-edit
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_edit:cli
+    :prog: sshkey edit
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_list:cli
+    :prog: security sshkey-list
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_list:cli
+    :prog: sshkey list
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_print:cli
+    :prog: security sshkey-print
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.sshkey_print:cli
+    :prog: sshkey print
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_add:cli
+    :prog: security cert-add
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_add:cli
+    :prog: ssl add
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_download:cli
+    :prog: security cert-download
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_download:cli
+    :prog: ssl download
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_edit:cli
+    :prog: security cert-edit
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_edit:cli
+    :prog: ssl edit
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_list:cli
+    :prog: security cert-list
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_list:cli
+    :prog: ssl list
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_remove:cli
+    :prog: security cert-remove
+    :show-nested:
+
+.. click:: SoftLayer.CLI.security.cert_remove:cli
+    :prog: ssl remove
+    :show-nested:
diff --git a/docs/cli/sshkey.rst b/docs/cli/sshkey.rst
index d12d2f42..91a7b8c8 100644
--- a/docs/cli/sshkey.rst
+++ b/docs/cli/sshkey.rst
@@ -4,21 +4,26 @@ SSH Keys
 ========
 
 .. click:: SoftLayer.CLI.sshkey.add:cli
+    This command is an alias for `slcli security sshkey-add`
     :prog: sshkey add
     :show-nested:
 
 .. click:: SoftLayer.CLI.sshkey.remove:cli
+    This command is an alias for `slcli security sshkey-remove`
     :prog: sshkey remove
     :show-nested:
 
 .. click:: SoftLayer.CLI.sshkey.edit:cli
+    This command is an alias for `slcli security sshkey-edit`
     :prog: sshkey edit
     :show-nested:
 
 .. click:: SoftLayer.CLI.sshkey.list:cli
+    This command is an alias for `slcli security sshkey-list`
     :prog: sshkey list
     :show-nested:
 
 .. click:: SoftLayer.CLI.sshkey.print:cli
+    This command is an alias for `slcli security sshkey-print`
     :prog: sshkey print
     :show-nested:
diff --git a/docs/cli/ssl.rst b/docs/cli/ssl.rst
index f8c95f05..1b85dc77 100644
--- a/docs/cli/ssl.rst
+++ b/docs/cli/ssl.rst
@@ -4,22 +4,27 @@ SSL Certificates
 ================
 
 .. click:: SoftLayer.CLI.ssl.add:cli
+    This command is an alias for `slcli security cert_add`
     :prog: ssl add
     :show-nested:
 
 .. click:: SoftLayer.CLI.ssl.download:cli
+    This command is an alias for `slcli security cert_download`
     :prog: ssl download
     :show-nested:
 
 .. click:: SoftLayer.CLI.ssl.edit:cli
+    This command is an alias for `slcli security cert_edit`
     :prog: ssl edit
     :show-nested:
 
 .. click:: SoftLayer.CLI.ssl.list:cli
+    This command is an alias for `slcli security cert_list`
     :prog: ssl list
     :show-nested:
 
 .. click:: SoftLayer.CLI.ssl.remove:cli
+    This command is an alias for `slcli security cert_remove`
     :prog: ssl remove
     :show-nested:
 


Test patch:
diff --git a/tests/CLI/modules/security_tests.py b/tests/CLI/modules/security_tests.py
new file mode 100644
index 00000000..8e14ac33
--- /dev/null
+++ b/tests/CLI/modules/security_tests.py
@@ -0,0 +1,154 @@
+"""
+    SoftLayer.tests.CLI.modules.security_tests
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    :license: MIT, see LICENSE for more details.
+"""
+import json
+import os.path
+import sys
+import tempfile
+
+from unittest import mock as mock
+
+from SoftLayer.CLI import exceptions
+from SoftLayer import testing
+
+
+class SecurityTests(testing.TestCase):
+    def test_add_sshkey_without_key_errors(self):
+        result = self.run_command(['security', 'sshkey-add', 'key1'])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_add_sshkey_with_key_file_and_key_argument_errors(self):
+        path = os.path.join(testing.FIXTURE_PATH, 'id_rsa.pub')
+        result = self.run_command(['security', 'sshkey-add', 'key1',
+                                   '--key=some_key',
+                                   '--in-file=%s' % path])
+
+        self.assertEqual(result.exit_code, 2)
+        self.assertIsInstance(result.exception, exceptions.ArgumentError)
+
+    def test_add_sshkey_by_option(self):
+        service = self.client['Security_Ssh_Key']
+        mock_key = service.getObject()['key']
+
+        result = self.run_command(['security', 'sshkey-add', 'key1',
+                                   '--key=%s' % mock_key,
+                                   '--note=my key'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output),
+                         "SSH key added: aa:bb:cc:dd")
+        self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject',
+                                args=({'notes': 'my key',
+                                       'key': mock_key,
+                                       'label': 'key1'},))
+
+    def test_add_sshkey_by_file(self):
+        path = os.path.join(testing.FIXTURE_PATH, 'id_rsa.pub')
+
+        result = self.run_command(['security', 'sshkey-add', 'key1',
+                                   '--in-file=%s' % path])
+
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output),
+                         "SSH key added: aa:bb:cc:dd")
+        service = self.client['Security_Ssh_Key']
+        mock_key = service.getObject()['key']
+        self.assert_called_with('SoftLayer_Security_Ssh_Key', 'createObject',
+                                args=({'notes': None,
+                                       'key': mock_key,
+                                       'label': 'key1'},))
+
+    def test_remove_sshkey_key(self):
+        result = self.run_command(['--really', 'security', 'sshkey-remove', '1234'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Security_Ssh_Key', 'deleteObject',
+                                identifier=1234)
+
+    @mock.patch('SoftLayer.CLI.formatting.no_going_back')
+    def test_remove_sshkey_fail(self, ngb_mock):
+        ngb_mock.return_value = False
+        result = self.run_command(['security', 'sshkey-remove', '1234'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_edit_sshkey(self):
+        result = self.run_command(['security', 'sshkey-edit', '1234',
+                                   '--label=key1', '--note=my key'])
+
+        self.assert_no_fail(result)
+        self.assert_called_with('SoftLayer_Security_Ssh_Key', 'editObject',
+                                args=({'notes': 'my key',
+                                       'label': 'key1'},),
+                                identifier=1234)
+
+    def test_edit_sshkey_fail(self):
+        fixture = self.set_mock('SoftLayer_Security_Ssh_Key', 'editObject')
+        fixture.return_value = False
+
+        result = self.run_command(['security', 'sshkey-edit', '1234',
+                                   '--label=key1', '--note=my key'])
+
+        self.assertEqual(result.exit_code, 2)
+
+    def test_list_sshkeys(self):
+        result = self.run_command(['security', 'sshkey-list'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output),
+                         [{'notes': '-',
+                           'fingerprint': None,
+                           'id': '100',
+                           'label': 'Test 1'},
+                          {'notes': 'my key',
+                           'fingerprint': None,
+                           'id': '101',
+                           'label': 'Test 2'}])
+
+    def test_print_sshkey(self):
+        result = self.run_command(['security', 'sshkey-print', '1234'])
+
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output),
+                         {'id': 1234, 'label': 'label', 'notes': 'notes'})
+
+    def test_print_sshkey_file(self):
+        if sys.platform.startswith("win"):
+            self.skipTest("Test doesn't work in Windows")
+        with tempfile.NamedTemporaryFile() as sshkey_file:
+            service = self.client['Security_Ssh_Key']
+            mock_key = service.getObject()['key']
+            result = self.run_command(['security', 'sshkey-print', '1234',
+                                       '--out-file=%s' % sshkey_file.name])
+
+            self.assert_no_fail(result)
+            self.assertEqual(mock_key, sshkey_file.read().decode("utf-8"))
+
+    def test_list_certficates(self):
+        result = self.run_command(['security', 'cert-list', '--status', 'all'])
+        self.assert_no_fail(result)
+        self.assertEqual(json.loads(result.output), [
+            {
+                "id": 1234,
+                "common_name": "cert",
+                "days_until_expire": 0,
+                "notes": None
+            }
+        ])
+
+    @mock.patch('SoftLayer.CLI.formatting.no_going_back')
+    def test_remove_certficate(self, confirm_mock):
+        confirm_mock.return_value = True
+        result = self.run_command(['security', 'cert-remove', '123456'])
+        self.assert_no_fail(result)
+        self.assertEqual(result.exit_code, 0)
+
+    def test_download_certficate(self):
+        result = self.run_command(['security', 'cert-download', '123456'])
+        self.assert_no_fail(result)
+        self.assertEqual(result.exit_code, 0)
diff --git a/tests/CLI/modules/ssl_tests.py b/tests/CLI/modules/ssl_tests.py
index 2bcdff5c..73f2234c 100644
--- a/tests/CLI/modules/ssl_tests.py
+++ b/tests/CLI/modules/ssl_tests.py
@@ -1,7 +1,6 @@
 """
     SoftLayer.tests.CLI.modules.ssl_tests
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
     :license: MIT, see LICENSE for more details.
 """
 from SoftLayer import testing