Secret Management Service feature 15/66815/1
authorDileep Ranganathan <dileep.ranganathan@intel.com>
Sat, 15 Sep 2018 18:05:19 +0000 (11:05 -0700)
committerDileep Ranganathan <dileep.ranganathan@intel.com>
Sat, 15 Sep 2018 23:42:14 +0000 (16:42 -0700)
Added supporting library required for enabling SMS integration.
Added Unit tests and manual tests for store/retrieve/delete secrets.
Updated conductor.conf with aaf_sms group. Added preload_secrets
config for testing.

Integration with application NOT Done in this patch.

Change-Id: Idf7e4249a88a39c586d893226a9110e9d5180787
Issue-ID: OPTFRA-345
Signed-off-by: Dileep Ranganathan <dileep.ranganathan@intel.com>
conductor.conf
conductor/conductor/common/config_loader.py [new file with mode: 0644]
conductor/conductor/common/sms.py [new file with mode: 0644]
conductor/conductor/opts.py
conductor/conductor/tests/unit/test_sms.py [new file with mode: 0644]
conductor/requirements.txt
conductor/test-requirements.txt
preload_secrets.yaml [new file with mode: 0755]

index a2a4765..b4e74e4 100755 (executable)
@@ -120,6 +120,28 @@ log_config_append = /usr/local/bin/log.conf
 #fatal_deprecations = false
 
 
+[aaf_sms]
+
+#
+# From conductor
+#
+
+# Base URL for SMS, up to and not including the version, and without a trailing
+# slash. (string value)
+#aaf_sms_url = https://aaf-sms.onap:10443
+
+# Timeout for SMS API Call (integer value)
+#aaf_sms_timeout = 30
+
+# Path to the cacert that will be used to verify If this is None, verify will
+# be False and the server certis not verified by the client. (string value)
+#aaf_ca_certs = AAF_RootCA.cer
+
+# Domain UUID - A unique UUID generated when the domainfor HAS is created by
+# administrator during deployment (string value)
+#secret_domain = has
+
+
 [aai]
 
 #
diff --git a/conductor/conductor/common/config_loader.py b/conductor/conductor/common/config_loader.py
new file mode 100644 (file)
index 0000000..60e05f1
--- /dev/null
@@ -0,0 +1,38 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 Intel Corporation Intellectual Property
+#
+#   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 json
+
+import yaml
+
+
+def load_config_file(config_file, child_name="dockerConfiguration"):
+    """
+    Load YAML/JSON configuration from a file
+    :param config_file:  path to config file (.yaml or .json).
+    :param child_name: if present, return only that child node
+    :return: config (all or specific child node)
+    """
+    with open(config_file, 'r') as fid:
+        res = {}
+        if config_file.endswith(".yaml"):
+            res = yaml.load(fid)
+        elif config_file.endswith(".json") or config_file.endswith("json"):
+            res = json.load(fid)
+    return res.get(child_name, res) if child_name else res
diff --git a/conductor/conductor/common/sms.py b/conductor/conductor/common/sms.py
new file mode 100644 (file)
index 0000000..43b9522
--- /dev/null
@@ -0,0 +1,120 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 Intel Corporation Intellectual Property
+#
+#   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.
+#
+# -------------------------------------------------------------------------
+#
+
+'''Secret Management Service Integration'''
+from conductor.common import config_loader
+from onapsmsclient import Client
+
+from oslo_config import cfg
+from oslo_log import log
+
+LOG = log.getLogger(__name__)
+
+CONF = cfg.CONF
+
+AAF_SMS_OPTS = [
+    cfg.StrOpt('aaf_sms_url',
+               default='https://aaf-sms.onap:10443',
+               help='Base URL for SMS, up to and not including '
+                    'the version, and without a trailing slash.'),
+    cfg.IntOpt('aaf_sms_timeout',
+               default=30,
+               help='Timeout for SMS API Call'),
+    cfg.StrOpt('aaf_ca_certs',
+               default='AAF_RootCA.cer',
+               help='Path to the cacert that will be used to verify '
+                    'If this is None, verify will be False and the server cert'
+                    'is not verified by the client.'),
+    cfg.StrOpt('secret_domain',
+               default='has',
+               help='Domain UUID - A unique UUID generated when the domain'
+                    'for HAS is created by administrator during deployment')
+]
+
+CONF.register_opts(AAF_SMS_OPTS, group='aaf_sms')
+config_spec = {
+    "preload_secrets": "../preload_secrets.yaml"
+}
+
+secret_cache = {}
+
+
+def preload_secrets():
+    """ This is intended to load the secrets required for testing Application
+        Actual deployment will have a preload script. Make sure the config is
+        in sync"""
+    preload_config = config_loader.load_config_file(
+        config_spec.get("preload_secrets"))
+    domain = preload_config.get("domain")
+    config = CONF.aaf_sms
+    sms_url = config.aaf_sms_url
+    timeout = config.aaf_sms_timeout
+    cacert = config.aaf_ca_certs
+    sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert)
+    domain = sms_client.createDomain(domain)
+    config.secret_domain = domain  # uuid
+    secrets = preload_config.get("secrets")
+    for secret in secrets:
+        sms_client.storeSecret(domain, secret.get('name'),
+                               secret.get('values'))
+    LOG.debug("Preload secrets complete")
+
+
+def retrieve_secrets():
+    """Get all secrets under the domain name"""
+    secret_dict = dict()
+    config = CONF.aaf_sms
+    sms_url = config.aaf_sms_url
+    timeout = config.aaf_sms_timeout
+    cacert = config.aaf_ca_certs
+    domain = config.secret_domain
+    sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert)
+    secrets = sms_client.getSecretNames(domain)
+    for secret in secrets:
+        values = sms_client.getSecret(domain, secret)
+        secret_dict[secret] = values
+    LOG.debug("Secret Dictionary Retrieval Success")
+    return secret_dict
+
+
+def delete_secrets():
+    """ This is intended to delete the secrets for a clean initialization for
+        testing Application. Actual deployment will have a preload script.
+        Make sure the config is in sync"""
+    config = CONF.aaf_sms
+    sms_url = config.aaf_sms_url
+    timeout = config.aaf_sms_timeout
+    cacert = config.aaf_ca_certs
+    domain = config.secret_domain
+    sms_client = Client(url=sms_url, timeout=timeout, cacert=cacert)
+    ret_val = sms_client.deleteDomain(domain)
+    LOG.debug("Clean up complete")
+    return ret_val
+
+
+if __name__ == "__main__":
+    # Initialize Secrets from SMS
+    preload_secrets()
+
+    # Retrieve Secrets from SMS and load to secret cache
+    # Use the secret_cache instead of config files
+    secret_cache = retrieve_secrets()
+
+    # Clean up Delete secrets and domain
+    delete_secrets()
index e2ace38..52624cf 100644 (file)
@@ -22,6 +22,7 @@ import itertools
 import conductor.api.app
 import conductor.common.music.api
 import conductor.common.music.messaging.component
+import conductor.common.sms
 import conductor.conf.inventory_provider
 import conductor.conf.service_controller
 import conductor.conf.vim_controller
@@ -68,4 +69,5 @@ def list_opts():
         ('music_api', conductor.common.music.api.MUSIC_API_OPTS),
         ('solver', conductor.solver.service.SOLVER_OPTS),
         ('reservation', conductor.reservation.service.reservation_OPTS),
+        ('aaf_sms', conductor.common.sms.AAF_SMS_OPTS),
     ]
diff --git a/conductor/conductor/tests/unit/test_sms.py b/conductor/conductor/tests/unit/test_sms.py
new file mode 100644 (file)
index 0000000..b04111e
--- /dev/null
@@ -0,0 +1,89 @@
+#
+# -------------------------------------------------------------------------
+#   Copyright (c) 2018 Intel Corporation Intellectual Property
+#
+#   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 unittest
+from uuid import uuid4
+
+import requests_mock
+
+import conductor.common.sms as SMS
+from oslo_config import cfg
+
+
+class TestSMS(unittest.TestCase):
+
+    def setUp(self):
+        self.config = cfg.CONF.aaf_sms
+        self.base_domain_url = '{}/v1/sms/domain'
+        self.domain_url = '{}/v1/sms/domain/{}'
+        self.secret_url = self.domain_url + '/secret'
+
+    @requests_mock.mock()
+    def test_sms(self, mock_sms):
+        ''' NOTE: preload_secret generate the uuid for the domain
+                  Create Domain API is called during the deployment using a
+                  preload script. So the application oly knows the domain_uuid.
+                  All sub-sequent SMS API calls needs the uuid.
+                  For test purposes we need to do preload ourselves'''
+        sms_url = self.config.aaf_sms_url
+
+        # JSON Data responses
+        secretnames = {'secretnames': ['s1', 's2', 's3', 's4']}
+        secretvalues = {'values': {'Password': '', 'UserName': ''}}
+        expecect_secret_dict = dict()
+        for secret in secretnames['secretnames']:
+            expecect_secret_dict[secret] = secretvalues['values']
+
+        # Part 1 : Preload Secrets ONLY FOR TEST
+        # Mock requests for preload_secret
+        cd_url = self.base_domain_url.format(sms_url)
+        domain_uuid1 = str(uuid4())
+        s_url = self.secret_url.format(sms_url, domain_uuid1)
+        mock_sms.post(cd_url, status_code=200, json={'uuid': domain_uuid1})
+        mock_sms.post(s_url, status_code=200)
+        # Initialize Secrets from SMS
+        SMS.preload_secrets()
+
+        # Part 2: Retrieve Secret Test
+        # Mock requests for retrieve_secrets
+        # IMPORTANT: Read the config again as the preload_secrets has
+        # updated the config with uuid
+        domain_uuid2 = self.config.secret_domain
+        self.assertEqual(domain_uuid1, domain_uuid2)
+
+        d_url = self.domain_url.format(sms_url, domain_uuid2)
+        s_url = self.secret_url.format(sms_url, domain_uuid2)
+
+        # Retrieve Secrets from SMS and load to secret cache
+        # Use the secret_cache instead of config files
+        mock_sms.get(s_url, status_code=200, json=secretnames)
+        for secret in secretnames['secretnames']:
+            mock_sms.get('{}/{}'.format(s_url, secret),
+                         status_code=200, json=secretvalues)
+        secret_cache = SMS.retrieve_secrets()
+        self.assertDictEqual(expecect_secret_dict, secret_cache,
+                             'Failed to retrieve secrets')
+
+        # Part 3: Clean up Delete secrets and domain
+        # Mock requests for delete_secrets
+        mock_sms.delete(d_url, status_code=200)
+        self.assertTrue(SMS.delete_secrets())
+
+
+if __name__ == "__main__":
+    unittest.main()
index 9359e26..d09c960 100644 (file)
@@ -23,3 +23,4 @@ requests[security]!=2.9.0,>=2.8.1 # Apache-2.0
 six>=1.9.0 # MIT, also required by futurist
 stevedore>=1.9.0 # Apache-2.0, also required by oslo.config
 WebOb>=1.2.3 # MIT
+onapsmsclient>=0.0.3
\ No newline at end of file
index c0e68d0..7466c9d 100644 (file)
@@ -18,3 +18,4 @@ os-testr>=1.0.0 # Apache-2.0
 tempest>=11.0.0  # Apache-2.0
 pifpaf>=0.0.11
 junitxml>=0.7
+requests-mock>=1.5.2
diff --git a/preload_secrets.yaml b/preload_secrets.yaml
new file mode 100755 (executable)
index 0000000..65a814a
--- /dev/null
@@ -0,0 +1,21 @@
+# DO NOT USE THIS IN PRODUCTION
+# USED ONLY FOR TESTING
+---
+domain: has
+secrets:
+- name: aai
+  values:
+    UserName: OOF
+    Password: OOF
+- name: conductor_api
+  values:
+    UserName: admin1
+    Password: plan.15
+- name: sdnc
+  values:
+    UserName: admin
+    Password: sdnc.15
+- name: music_api
+  values:
+    UserName: conductor
+    Password: c0nduct0r