From 69d5fbb7b4bbf234f38542bca6134f167b929aa8 Mon Sep 17 00:00:00 2001 From: Dileep Ranganathan Date: Sat, 15 Sep 2018 11:05:19 -0700 Subject: Secret Management Service feature 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 --- conductor/conductor/common/config_loader.py | 38 +++++++++ conductor/conductor/common/sms.py | 120 ++++++++++++++++++++++++++++ conductor/conductor/opts.py | 2 + conductor/conductor/tests/unit/test_sms.py | 89 +++++++++++++++++++++ conductor/requirements.txt | 1 + conductor/test-requirements.txt | 1 + 6 files changed, 251 insertions(+) create mode 100644 conductor/conductor/common/config_loader.py create mode 100644 conductor/conductor/common/sms.py create mode 100644 conductor/conductor/tests/unit/test_sms.py (limited to 'conductor') diff --git a/conductor/conductor/common/config_loader.py b/conductor/conductor/common/config_loader.py new file mode 100644 index 0000000..60e05f1 --- /dev/null +++ b/conductor/conductor/common/config_loader.py @@ -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 index 0000000..43b9522 --- /dev/null +++ b/conductor/conductor/common/sms.py @@ -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() diff --git a/conductor/conductor/opts.py b/conductor/conductor/opts.py index e2ace38..52624cf 100644 --- a/conductor/conductor/opts.py +++ b/conductor/conductor/opts.py @@ -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 index 0000000..b04111e --- /dev/null +++ b/conductor/conductor/tests/unit/test_sms.py @@ -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() diff --git a/conductor/requirements.txt b/conductor/requirements.txt index 9359e26..d09c960 100644 --- a/conductor/requirements.txt +++ b/conductor/requirements.txt @@ -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 diff --git a/conductor/test-requirements.txt b/conductor/test-requirements.txt index c0e68d0..7466c9d 100644 --- a/conductor/test-requirements.txt +++ b/conductor/test-requirements.txt @@ -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 -- cgit 1.2.3-korg