diff options
41 files changed, 674 insertions, 282 deletions
diff --git a/.coveragerc b/.coveragerc index a5afd52..a4ec20c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,7 @@ [run] branch = True cover_pylib = False -include = osdf/**/*.py +include = osdf/**/*.py, apps/**/*.py [report] # Regexes for lines to exclude from consideration @@ -29,6 +29,9 @@ wheels/ .installed.cfg *.egg MANIFEST +AUTHORS +ChangeLog +logs/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3797dc8 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,20 @@ +--- +# .readthedocs.yml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# Required +version: 2 + +formats: + - htmlzip + +build: + image: latest + +python: + version: 3.7 + install: + - requirements: docs/requirements-docs.txt + +sphinx: + configuration: docs/conf.py diff --git a/osdf/optimizers/__init__.py b/README.md index 4b25e5b..0e2641a 100644 --- a/osdf/optimizers/__init__.py +++ b/README.md @@ -1,5 +1,6 @@ +# # ------------------------------------------------------------------------- -# Copyright (c) 2017-2018 AT&T Intellectual Property +# Copyright (C) 2020 Wipro Limited. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,3 +16,13 @@ # # ------------------------------------------------------------------------- # + + +#osdf + + +#cipher-utility + + python3 setup.py install + export PYTHONPATH=$PYTHONPATH:`pwd` + diff --git a/apps/pci/models/api/pciOptimizationRequest.py b/apps/pci/models/api/pciOptimizationRequest.py index 02b67a2..2aa22f1 100644 --- a/apps/pci/models/api/pciOptimizationRequest.py +++ b/apps/pci/models/api/pciOptimizationRequest.py @@ -45,6 +45,8 @@ class CellInfo(OSDFModel): networkId = StringType(required=True) cellIdList = ListType(StringType(required=True)) anrInputList = ListType(ModelType(ANRInfo)) + fixedPCICells = ListType(StringType()) + priorityTreatmentCells = ListType(StringType()) trigger = StringType() diff --git a/apps/placement/optimizers/conductor/translation.py b/apps/placement/optimizers/conductor/translation.py index d361755..46bee1d 100644 --- a/apps/placement/optimizers/conductor/translation.py +++ b/apps/placement/optimizers/conductor/translation.py @@ -17,10 +17,10 @@ # import copy import json -import yaml import re -from osdf.utils.data_conversion import text_to_symbol +import yaml + from osdf.utils.programming_utils import dot_notation policy_config_mapping = yaml.safe_load(open('config/has_config.yaml')).get('policy_config_mapping') @@ -157,7 +157,7 @@ def gen_attribute_policy(vnf_list, attribute_policy): cur_policies, related_policies = gen_policy_instance(vnf_list, attribute_policy, rtype=None) for p_new, p_main in zip(cur_policies, related_policies): # add additional fields to each policy properties = p_main['content']['cloudAttributeProperty'] - attribute_mapping = policy_config_mapping['attributes'] # wanted attributes and mapping + attribute_mapping = policy_config_mapping['filtering_attributes'] # wanted attributes and mapping p_new[p_main['content']['identity']]['properties'] = { 'evaluate': dict((k, properties.get(attribute_mapping.get(k))) for k in attribute_mapping.keys()) } @@ -231,42 +231,47 @@ def get_demand_properties(demand, policies): prop.update({'unique': policy_property['unique']} if 'unique' in policy_property and policy_property['unique'] else {}) - prop['attributes'] = dict() - prop['attributes'].update({'global-customer-id': policy_property['customerId']} + prop['filtering_attributes'] = dict() + prop['filtering_attributes'].update({'global-customer-id': policy_property['customerId']} if policy_property['customerId'] else {}) - prop['attributes'].update({'model-invariant-id': demand['resourceModelInfo']['modelInvariantId']} + prop['filtering_attributes'].update({'model-invariant-id': demand['resourceModelInfo']['modelInvariantId']} if demand['resourceModelInfo']['modelInvariantId'] else {}) - prop['attributes'].update({'model-version-id': demand['resourceModelInfo']['modelVersionId']} + prop['filtering_attributes'].update({'model-version-id': demand['resourceModelInfo']['modelVersionId']} if demand['resourceModelInfo']['modelVersionId'] else {}) - prop['attributes'].update({'equipment-role': policy_property['equipmentRole']} + prop['filtering_attributes'].update({'equipment-role': policy_property['equipmentRole']} if policy_property['equipmentRole'] else {}) if policy_property.get('attributes'): for attr_key, attr_val in policy_property['attributes'].items(): - update_converted_attribute(attr_key, attr_val, prop) + update_converted_attribute(attr_key, attr_val, prop, 'filtering_attributes') + if policy_property.get('passthroughAttributes'): + prop['passthrough_attributes'] = dict() + for attr_key, attr_val in policy_property['passthroughAttributes'].items(): + update_converted_attribute(attr_key, attr_val, prop, 'passthrough_attributes') prop.update(get_candidates_demands(demand)) demand_properties.append(prop) return demand_properties -def update_converted_attribute(attr_key, attr_val, properties): +def update_converted_attribute(attr_key, attr_val, properties, attribute_type): """ Updates dictonary of attributes with one specified in the arguments. Automatically translates key namr from camelCase to hyphens + :param attribute_type: attribute section name :param attr_key: key of the attribute :param attr_val: value of the attribute :param properties: dictionary with attributes to update :return: """ if attr_val: - remapping = policy_config_mapping['attributes'] + remapping = policy_config_mapping[attribute_type] if remapping.get(attr_key): key_value = remapping.get(attr_key) else: key_value = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', attr_key) key_value = re.sub('([a-z0-9])([A-Z])', r'\1-\2', key_value).lower() - properties['attributes'].update({key_value: attr_val}) + properties[attribute_type].update({key_value: attr_val}) def gen_demands(req_json, vnf_policies): diff --git a/config/has_config.yaml b/config/has_config.yaml index 38a4781..2371508 100644 --- a/config/has_config.yaml +++ b/config/has_config.yaml @@ -1,5 +1,5 @@ policy_config_mapping: - attributes: + filtering_attributes: hypervisor: hypervisor cloudVersion: cloud_version cloudType: cloud_type @@ -19,6 +19,7 @@ policy_config_mapping: cloudRegionId: cloud-region-id orchestrationStatus: orchestration-status provStatus: prov-status + passthrough_attributes: {} candidates: # for (k1, v1), if k1 is in demand, set prop[k2] = _get_candidates(demand[k1]) excludedCandidates: excluded_candidates diff --git a/config/osdf_config.yaml b/config/osdf_config.yaml index 6cf8cec..eba89e0 100755 --- a/config/osdf_config.yaml +++ b/config/osdf_config.yaml @@ -51,4 +51,7 @@ configDbGetCellListUrl: 'SDNCConfigDBAPI/getCellList' configDbGetNbrListUrl: 'SDNCConfigDBAPI/getNbrList' pciHMSUsername: test -pciHMSPassword: passwd
\ No newline at end of file +pciHMSPassword: passwd + +#key +appkey: os35@rrtky400fdntc#001t5
\ No newline at end of file diff --git a/config/preload_secrets.yaml b/config/preload_secrets.yaml index 3050d87..0bb2395 100755 --- a/config/preload_secrets.yaml +++ b/config/preload_secrets.yaml @@ -1,51 +1,51 @@ --- domain: osdf secrets: -- name: so - values: - UserName: '' - Password: '' -- name: conductor - values: - UserName: admin1 - Password: plan.15 -- name: policyPlatform - values: - UserName: healthcheck - Password: zb!XztG34 -- name: dmaap - values: - UserName: NA - Password: NA -- name: sdc - values: - UserName: NA - Password: NA -- name: osdfPlacement - values: - UserName: test - Password: testpwd -- name: osdfPlacementSO - values: - UserName: so_test - Password: so_testpwd -- name: osdfPlacementVFC - values: - UserName: vfc_test - Password: vfc_testpwd -- name: osdfCMScheduler - values: - UserName: test1 - Password: testpwd1 -- name: configDb - values: - UserName: osdf - Password: passwd -- name: pciHMS - values: - UserName: '' - Password: '' -- name: osdfPCIOpt - values: - UserName: pci_test - Password: pci_testpwd + - name: so + values: + UserName: '' + Password: '' + - name: conductor + values: + UserName: admin1 + Password: 22234d3472ef5da8ecba5a096110a024f1db5cf195c665f910d558c9e83db19d + - name: policyPlatform + values: + UserName: healthcheck + Password: 49a03554e86ecdb8e9e224127791c579b44993b264549a333172af77c2ae95fc + - name: dmaap + values: + UserName: NA + Password: NA + - name: sdc + values: + UserName: NA + Password: NA + - name: osdfPlacement + values: + UserName: test + Password: c66b1570ae257375e500f9fe0e62b2a325466137ac5f29581e2e05cce1170212 + - name: osdfPlacementSO + values: + UserName: so_test + Password: 3d62d49b3e4ada38fd4146d2d82f4ba2f09345a46f15970cd439924c991b8202 + - name: osdfPlacementVFC + values: + UserName: vfc_test + Password: 1fb1cd581f96060d29ecad06be97151656bf29bce66bad587cd2fbaf5ea1e66d + - name: osdfCMScheduler + values: + UserName: test1 + Password: c5279fb02d7bac5269b1a644ac8e36f41f6ba7a2eae03dc469cb80d71811322b + - name: configDb + values: + UserName: osdf + Password: 40697f254409c2b97763892ecdeb50c847d605f5beb6f988f1c142a7e0344d0c + - name: pciHMS + values: + UserName: '' + Password: '' + - name: osdfPCIOpt + values: + UserName: pci_test + Password: fbf4dcb7f7cda8fdfb742838b0c90ae5bea249801f3f725fdc98941a6e4c347c diff --git a/docker/Dockerfile b/docker/Dockerfile index 0f271c8..e339ea7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,7 +30,7 @@ ENV https_proxy $HTTPS_PROXY ENV OSDF_PORT "8699" EXPOSE ${OSDF_PORT} -ENV MZN 2.3.2 +ENV MZN 2.4.2 ENV MZN_BASENAME MiniZincIDE-${MZN}-bundle-linux ENV MZN_GH_BASE https://github.com/MiniZinc/MiniZincIDE ENV MZN_DL_URL ${MZN_GH_BASE}/releases/download/${MZN}/${MZN_BASENAME}-x86_64.tgz @@ -49,8 +49,7 @@ RUN wget -q $MZN_DL_URL -O /tmp/mz.tgz \ && tar xzf /tmp/mz.tgz \ && mv $MZN_BASENAME /mz-dist \ && rm /tmp/mz.tgz \ - && echo PATH=/mz-dist/bin:$PATH >> ~/.bashrc \ - && echo 'export LD_LIBRARY_PATH=/mz-dist/lib:LD_LIBRARY_PATH' >> ~/.bashrc + && echo PATH=/mz-dist/bin:$PATH >> ~/.bashrc ENV SHELL /bin/bash ENV PATH /mz-dist:$PATH diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..43ca5b6 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +/.tox +/_build/* +/__pycache__/* diff --git a/docs/_static/css/ribbon.css b/docs/_static/css/ribbon.css new file mode 100644 index 0000000..6008cb1 --- /dev/null +++ b/docs/_static/css/ribbon.css @@ -0,0 +1,63 @@ +.ribbon { + z-index: 1000; + background-color: #a00; + overflow: hidden; + white-space: nowrap; + position: fixed; + top: 25px; + right: -50px; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + -webkit-box-shadow: 0 0 10px #888; + -moz-box-shadow: 0 0 10px #888; + box-shadow: 0 0 10px #888; + +} + +.ribbon a { + border: 1px solid #faa; + color: #fff; + display: block; + font: bold 81.25% 'Helvetica Neue', Helvetica, Arial, sans-serif; + margin: 1px 0; + padding: 10px 50px; + text-align: center; + text-decoration: none; + text-shadow: 0 0 5px #444; + transition: 0.5s; +} + +.ribbon a:hover { + background: #c11; + color: #fff; +} + + +/* override table width restrictions */ +@media screen and (min-width: 767px) { + + .wy-table-responsive table td, .wy-table-responsive table th { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + } +} + +@media screen and (max-width: 767px) { + .wy-table-responsive table td { + white-space: nowrap; + } +} + +/* fix width of the screen */ + +.wy-nav-content { + max-width: none; +} diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico Binary files differnew file mode 100755 index 0000000..cb712eb --- /dev/null +++ b/docs/_static/favicon.ico diff --git a/docs/_static/logo_onap_2017.png b/docs/_static/logo_onap_2017.png Binary files differnew file mode 100644 index 0000000..5d064f4 --- /dev/null +++ b/docs/_static/logo_onap_2017.png diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..8f40e8b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,15 @@ +from docs_conf.conf import * + +branch = 'latest' +master_doc = 'index' + +linkcheck_ignore = [ + 'http://localhost', +] + +intersphinx_mapping = {} + +html_last_updated_fmt = '%d-%b-%y %H:%M' + +def setup(app): + app.add_stylesheet("css/ribbon_onap.css") diff --git a/docs/conf.yaml b/docs/conf.yaml new file mode 100644 index 0000000..ab59281 --- /dev/null +++ b/docs/conf.yaml @@ -0,0 +1,7 @@ +--- +project_cfg: onap +project: onap + +# Change this to ReleaseBranchName to modify the header +default-version: latest +# diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt new file mode 100644 index 0000000..b3188dd --- /dev/null +++ b/docs/requirements-docs.txt @@ -0,0 +1,15 @@ +tox +Sphinx +doc8 +docutils +setuptools +six +sphinx_rtd_theme>=0.4.3 +sphinxcontrib-blockdiag +sphinxcontrib-needs>=0.2.3 +sphinxcontrib-nwdiag +sphinxcontrib-seqdiag +sphinxcontrib-swaggerdoc +sphinxcontrib-plantuml +sphinx_bootstrap_theme +lfdocs-conf diff --git a/docs/sections/swaggerdoc/oof-osdf-has-api.json b/docs/sections/swaggerdoc/oof-osdf-has-api.json index e52c39b..b2b90a8 100644 --- a/docs/sections/swaggerdoc/oof-osdf-has-api.json +++ b/docs/sections/swaggerdoc/oof-osdf-has-api.json @@ -26,6 +26,9 @@ "paths": { "/v2/placement": { "post": { + "tags": [ + "Placement Optimization" + ], "summary": "create/update a placement", "operationId": "createPlacement", "description": "create/update a placement", @@ -75,6 +78,9 @@ }, "/api/oof/v1/pci": { "post": { + "tags": [ + "PCI/ANR Optimization" + ], "summary": "Initiate PCI/ANR Optimization", "operationId": "initiatePCIOptRequest", "description": "Initiate PCI/ANR Optimization", @@ -124,6 +130,9 @@ }, "/api/oof/selection/nst/v1": { "post": { + "tags": [ + "NST Selection" + ], "summary": "NST selection", "operationId": "selectNstRequest", "description": "Request for NST selection", @@ -170,6 +179,9 @@ }, "/api/oof/selection/nsi/v1": { "post": { + "tags": [ + "NSI Selection" + ], "summary": "NSI selection", "operationId": "selectNsiRequest", "description": "Request for NSI selection", @@ -216,6 +228,9 @@ }, "/api/oof/selection/nssi/v1": { "post": { + "tags": [ + "NSSI Selection" + ], "summary": "NSSI selection", "operationId": "selectNssiRequest", "description": "Request for NSSI selection", @@ -1118,6 +1133,28 @@ }, "description": "A list of ANR Input." }, + "fixedPCICells": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of blacklisted cells whose PCI values should not be changed", + "example": [ + "cell0007", + "cell0009" + ] + }, + "priorityTreatmentCells": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of cells which should be given special treatment during optimization", + "example": [ + "cell0010", + "cell0003" + ] + }, "trigger": { "type": "string", "description": "Type of trigger causing need for PCI optimization", diff --git a/docs/tox.ini b/docs/tox.ini new file mode 100644 index 0000000..edac8c3 --- /dev/null +++ b/docs/tox.ini @@ -0,0 +1,22 @@ +[tox] +minversion = 1.6 +envlist = docs, +skipsdist = true + +[testenv:docs] +basepython = python3 +deps = -r{toxinidir}/requirements-docs.txt +commands = + sphinx-build -b html -n -d {envtmpdir}/doctrees ./ {toxinidir}/_build/html + echo "Generated docs available in {toxinidir}/_build/html" +whitelist_externals = + echo + git + sh + +[testenv:docs-linkcheck] +basepython = python3 +#deps = -r{toxinidir}/requirements-docs.txt +commands = echo "Link Checking not enforced" +#commands = sphinx-build -b linkcheck -d {envtmpdir}/doctrees ./ {toxinidir}/_build/linkcheck +whitelist_externals = echo diff --git a/osdf/adapters/aaf/sms.py b/osdf/adapters/aaf/sms.py index 25ae7f2..fd3a5d5 100644 --- a/osdf/adapters/aaf/sms.py +++ b/osdf/adapters/aaf/sms.py @@ -1,6 +1,7 @@ # # ------------------------------------------------------------------------- # Copyright (c) 2018 Intel Corporation Intellectual Property +# Copyright (C) 2020 Wipro Limited. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,12 +21,12 @@ '''Secret Management Service Integration''' from onapsmsclient import Client - import osdf.config.base as cfg_base import osdf.config.credentials as creds import osdf.config.loader as config_loader from osdf.config.base import osdf_config from osdf.logging.osdf_logging import debug_log +from osdf.utils import cipherUtils config_spec = { "preload_secrets": "config/preload_secrets.yaml" @@ -70,40 +71,46 @@ def retrieve_secrets(): debug_log.debug("Secret Dictionary Retrieval Success") return secret_dict - def load_secrets(): config = osdf_config.deployment secret_dict = retrieve_secrets() config['soUsername'] = secret_dict['so']['UserName'] - config['soPassword'] = secret_dict['so']['Password'] + config['soPassword'] = decrypt_pass(secret_dict['so']['Password']) config['conductorUsername'] = secret_dict['conductor']['UserName'] - config['conductorPassword'] = secret_dict['conductor']['Password'] + config['conductorPassword'] = decrypt_pass(secret_dict['conductor']['Password']) config['policyPlatformUsername'] = secret_dict['policyPlatform']['UserName'] - config['policyPlatformPassword'] = secret_dict['policyPlatform']['Password'] - config['policyClientUsername'] = secret_dict['policyClient']['UserName'] - config['policyClientPassword'] = secret_dict['policyClient']['Password'] + config['policyPlatformPassword'] = decrypt_pass(secret_dict['policyPlatform']['Password']) + config['policyClientUsername'] = secret_dict['policyPlatform']['UserName'] + config['policyClientPassword'] = decrypt_pass(secret_dict['policyPlatform']['Password']) config['messageReaderAafUserId'] = secret_dict['dmaap']['UserName'] - config['messageReaderAafPassword'] = secret_dict['dmaap']['Password'] + config['messageReaderAafPassword'] = decrypt_pass(secret_dict['dmaap']['Password']) config['sdcUsername'] = secret_dict['sdc']['UserName'] - config['sdcPassword'] = secret_dict['sdc']['Password'] + config['sdcPassword'] = decrypt_pass(secret_dict['sdc']['Password']) config['osdfPlacementUsername'] = secret_dict['osdfPlacement']['UserName'] - config['osdfPlacementPassword'] = secret_dict['osdfPlacement']['Password'] + config['osdfPlacementPassword'] = decrypt_pass(secret_dict['osdfPlacement']['Password']) config['osdfPlacementSOUsername'] = secret_dict['osdfPlacementSO']['UserName'] - config['osdfPlacementSOPassword'] = secret_dict['osdfPlacementSO']['Password'] + config['osdfPlacementSOPassword'] = decrypt_pass(secret_dict['osdfPlacementSO']['Password']) config['osdfPlacementVFCUsername'] = secret_dict['osdfPlacementVFC']['UserName'] - config['osdfPlacementVFCPassword'] = secret_dict['osdfPlacementVFC']['Password'] + config['osdfPlacementVFCPassword'] = decrypt_pass(secret_dict['osdfPlacementVFC']['Password']) config['osdfCMSchedulerUsername'] = secret_dict['osdfCMScheduler']['UserName'] - config['osdfCMSchedulerPassword'] = secret_dict['osdfCMScheduler']['Password'] + config['osdfCMSchedulerPassword'] = decrypt_pass(secret_dict['osdfCMScheduler']['Password']) config['configDbUserName'] = secret_dict['configDb']['UserName'] - config['configDbPassword'] = secret_dict['configDb']['Password'] + config['configDbPassword'] = decrypt_pass(secret_dict['configDb']['Password']) config['pciHMSUsername'] = secret_dict['pciHMS']['UserName'] - config['pciHMSPassword'] = secret_dict['pciHMS']['Password'] + config['pciHMSPassword'] = decrypt_pass(secret_dict['pciHMS']['Password']) config['osdfPCIOptUsername'] = secret_dict['osdfPCIOpt']['UserName'] - config['osdfPCIOptPassword'] = secret_dict['osdfPCIOpt']['Password'] + config['osdfPCIOptPassword'] = decrypt_pass(secret_dict['osdfPCIOpt']['Password']) cfg_base.http_basic_auth_credentials = creds.load_credentials(osdf_config) cfg_base.dmaap_creds = creds.dmaap_creds() +def decrypt_pass(passwd): + if passwd == '' or passwd == 'NA': + return passwd + else: + return cipherUtils.AESCipher.get_instance().decrypt(passwd) + + def delete_secrets(): """ This is intended to delete the secrets for a clean initialization for testing Application. Actual deployment will have a preload script. diff --git a/osdf/apps/baseapp.py b/osdf/apps/baseapp.py index cfa7e5d..008ce1d 100644 --- a/osdf/apps/baseapp.py +++ b/osdf/apps/baseapp.py @@ -27,18 +27,17 @@ import time import traceback from optparse import OptionParser -import pydevd -from flask import Flask, request, Response, g -from requests import RequestException -from schematics.exceptions import DataError - import osdf.adapters.aaf.sms as sms import osdf.operation.responses +import pydevd +from flask import Flask, request, Response, g from osdf.config.base import osdf_config from osdf.logging.osdf_logging import error_log, debug_log from osdf.operation.error_handling import request_exception_to_json_body, internal_error_message from osdf.operation.exceptions import BusinessException from osdf.utils.mdc_utils import clear_mdc, mdc_from_json, default_mdc +from requests import RequestException +from schematics.exceptions import DataError ERROR_TEMPLATE = osdf.ERROR_TEMPLATE @@ -90,17 +89,19 @@ def handle_data_error(e): @app.before_request def log_request(): g.request_start = time.clock() - if request.get_json(): - - request_json = request.get_json() - g.request_id = request_json['requestInfo']['requestId'] - mdc_from_json(request_json) + if request.data: + if request.get_json(): + request_json = request.get_json() + g.request_id = request_json['requestInfo']['requestId'] + mdc_from_json(request_json) + else: + g.request_id = "N/A" + default_mdc() else: g.request_id = "N/A" default_mdc() - @app.after_request def log_response(response): clear_mdc() diff --git a/osdf/cmd/encryptionUtil.py b/osdf/cmd/encryptionUtil.py new file mode 100644 index 0000000..6c0cae2 --- /dev/null +++ b/osdf/cmd/encryptionUtil.py @@ -0,0 +1,50 @@ +# +# ------------------------------------------------------------------------- +# Copyright (c) 2015-2018 AT&T 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 sys +from osdf.utils import cipherUtils + + +def main(): + + if len(sys.argv) != 4: + print("Invalid input - usage --> (options(encrypt/decrypt) input-value with-key)") + return + + enc_dec = sys.argv[1] + valid_option_values = ['encrypt', 'decrypt'] + if enc_dec not in valid_option_values: + print("Invalid input - usage --> (options(encrypt/decrypt) input-value with-key)") + print("Option value can only be one of {}".format(valid_option_values)) + print("You entered '{}'".format(enc_dec)) + return + + input_string = sys.argv[2] + with_key = sys.argv[3] + + print("You've requested '{}' to be '{}ed' using key '{}'".format(input_string, enc_dec, with_key)) + print("You can always perform the reverse operation (encrypt/decrypt) using the same key" + "to be certain you get the same results back'") + + util = cipherUtils.AESCipher.get_instance(with_key) + if enc_dec.lower() == 'encrypt': + result = util.encrypt(input_string) + else: + result = util.decrypt(input_string) + + print("Your resultt: {}".format(result))
\ No newline at end of file diff --git a/osdf/models/policy/placement/tosca/vnfPolicy-v20181031.yml b/osdf/models/policy/placement/tosca/vnfPolicy-v20181031.yml index 46d8c32..8eaf178 100644 --- a/osdf/models/policy/placement/tosca/vnfPolicy-v20181031.yml +++ b/osdf/models/policy/placement/tosca/vnfPolicy-v20181031.yml @@ -69,3 +69,17 @@ data_types: unique: type: string required: false + attributes: + type: list + required: false + entry_schema: + type:policy.data.vnfProperties_filteringAttributes + passthroughAttributes: + type: list + required: false + entry_schema: + type:policy.data.vnfProperties_passthroughAttributes + policy.data.vnfProperties_filteringAttributes: + derived_from: tosca.nodes.Root + policy.data.vnfProperties_passthroughAttributes: + derived_from: tosca.nodes.Root diff --git a/osdf/models/policy/placement/tosca_upload/onap.policies.optimization.VnfPolicy.yaml b/osdf/models/policy/placement/tosca_upload/onap.policies.optimization.VnfPolicy.yaml index e1ec36d..e242a92 100644 --- a/osdf/models/policy/placement/tosca_upload/onap.policies.optimization.VnfPolicy.yaml +++ b/osdf/models/policy/placement/tosca_upload/onap.policies.optimization.VnfPolicy.yaml @@ -73,4 +73,18 @@ data_types: unique: type: string required: false + attributes: + type: list + required: false + entry_schema: + type:policy.data.vnfProperties_filteringAttributes + passthroughAttributes: + type: list + required: false + entry_schema: + type:policy.data.vnfProperties_passthroughAttributes + policy.data.vnfProperties_filteringAttributes: + derived_from: tosca.nodes.Root + policy.data.vnfProperties_passthroughAttributes: + derived_from: tosca.nodes.Root diff --git a/osdf/optimizers/licenseopt/__init__.py b/osdf/optimizers/licenseopt/__init__.py deleted file mode 100644 index 4b25e5b..0000000 --- a/osdf/optimizers/licenseopt/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) 2017-2018 AT&T 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. -# -# ------------------------------------------------------------------------- -# diff --git a/osdf/optimizers/pciopt/__init__.py b/osdf/optimizers/pciopt/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/osdf/optimizers/pciopt/__init__.py +++ /dev/null diff --git a/osdf/optimizers/placementopt/__init__.py b/osdf/optimizers/placementopt/__init__.py deleted file mode 100644 index 4b25e5b..0000000 --- a/osdf/optimizers/placementopt/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) 2017-2018 AT&T 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. -# -# ------------------------------------------------------------------------- -# diff --git a/osdf/optimizers/routeopt/__init__.py b/osdf/optimizers/routeopt/__init__.py deleted file mode 100644 index c235f2a..0000000 --- a/osdf/optimizers/routeopt/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) 2018 Huawei 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. -# -# ------------------------------------------------------------------------- -# diff --git a/osdf/utils/cipherUtils.py b/osdf/utils/cipherUtils.py new file mode 100644 index 0000000..169f1a1 --- /dev/null +++ b/osdf/utils/cipherUtils.py @@ -0,0 +1,59 @@ +# +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- + +from Crypto.Cipher import AES +from osdf.config.base import osdf_config +from Crypto.Util.Padding import unpad +from Crypto.Util.Padding import pad + + +class AESCipher(object): + __instance = None + + @staticmethod + def get_instance(key = None): + if AESCipher.__instance is None: + print("Creating the singleton instance") + AESCipher(key) + return AESCipher.__instance + + def __init__(self, key=None): + if AESCipher.__instance is not None: + raise Exception("This class is a singleton!") + else: + AESCipher.__instance = self + + self.bs = 32 + if key is None: + key = osdf_config.deployment["appkey"] + + self.key = key.encode() + + def encrypt(self, data): + data = data.encode() + cipher = AES.new(self.key, AES.MODE_CBC) + ciphered_data = cipher.encrypt(pad(data, AES.block_size)) + enc = (cipher.iv.hex())+(ciphered_data.hex()) + return enc + + def decrypt(self, enc): + iv = bytes.fromhex(enc[:32]) + ciphered_data = bytes.fromhex(enc[32:]) + cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) + original_data = unpad(cipher.decrypt(ciphered_data), AES.block_size).decode() + return original_data diff --git a/osdf/utils/mdc_utils.py b/osdf/utils/mdc_utils.py index b98cbf0..bcd0615 100644 --- a/osdf/utils/mdc_utils.py +++ b/osdf/utils/mdc_utils.py @@ -36,7 +36,7 @@ def default_server_info(): MDC.put('server', server) if MDC.get('serverIPAddress') is None: try: - server_ip_address = socket.gethostbyname(self._fields['server']) + server_ip_address = socket.gethostbyname(MDC.get('server')) except Exception: server_ip_address = "" MDC.put('serverIPAddress', server_ip_address) diff --git a/osdf/webapp/appcontroller.py b/osdf/webapp/appcontroller.py index 9714fb5..e48e93f 100644 --- a/osdf/webapp/appcontroller.py +++ b/osdf/webapp/appcontroller.py @@ -35,6 +35,7 @@ error_body = { unauthorized_message = json.dumps(error_body) + @auth_basic.get_password def get_pw(username): end_point = request.url.split('/')[-1] @@ -42,6 +43,7 @@ def get_pw(username): return cfg_base.http_basic_auth_credentials[auth_group].get( username) if auth_group else None + @auth_basic.error_handler def auth_error(): response = Response(unauthorized_message, content_type='application/json; charset=utf-8') @@ -58,4 +60,3 @@ def verify_pw(username, password): else: pw = get_pw(username) return pw == password - return False
\ No newline at end of file @@ -21,7 +21,7 @@ <parent> <groupId>org.onap.oparent</groupId> <artifactId>oparent-python</artifactId> - <version>2.1.0</version> + <version>3.0.0</version> </parent> <groupId>org.onap.optf.osdf</groupId> @@ -34,11 +34,11 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <sonar.sources>.</sonar.sources> <sonar.junit.reportsPath>xunit-results.xml</sonar.junit.reportsPath> - <sonar.python.coverage.reportPath>coverage.xml</sonar.python.coverage.reportPath> + <sonar.python.coverage.reportPaths>coverage.xml</sonar.python.coverage.reportPaths> <sonar.language>py</sonar.language> <sonar.pluginname>python</sonar.pluginname> <sonar.inclusions>**/**.py,osdfapp.py</sonar.inclusions> - <sonar.exclusions>test/**.py</sonar.exclusions> + <sonar.exclusions>test/**.py,docs/**.py</sonar.exclusions> <maven.build.timestamp.format>yyyyMMdd'T'HHmmss'Z'</maven.build.timestamp.format> <osdf.build.timestamp>${maven.build.timestamp}</osdf.build.timestamp> <osdf.project.version>${project.version}</osdf.project.version> diff --git a/requirements.txt b/requirements.txt index 8001016..c3749e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ pydevd==1.4.0 onapsmsclient>=0.0.4 pymzn>=0.18.3 onappylog>=1.0.9 +pathtools>=0.1.2 +pycryptodome>=3.9.6 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1dffa77 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +# -*- encoding: utf-8 -*- +# ------------------------------------------------------------------------- +# Copyright (C) 2020 Wipro Limited. +# +# 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. +# +# ------------------------------------------------------------------------- +# + +'''Setup''' + +import setuptools + +setuptools.setup(name='of-osdf', + version='1.0', + description='Python Distribution Utilities', + author='xyz', + author_email='xyz@wipro.com', + url='https://wiki.onap.org/display/DW/Optimization+Service+Design+Framework', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: ONAP', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3' + 'Programming Language :: Python :: 3.5' + 'Topic :: Communications :: Email', + 'Topic :: Office/Business', + 'Topic :: Software Development :: Bug Tracking',], + keywords=['onap','osdf'], + packages=['osdf'], + entry_points = { + 'console_scripts': [ + 'cipher-utility = osdf.cmd.encryptionUtil:main', + ], + 'oslo.config.opts': [ + 'osdf = osdf.opts:list_opts', + ], + } + ) diff --git a/test/functest/simulators/build_sim_image.sh b/test/functest/simulators/build_sim_image.sh index 8efb273..6d6cb13 100755 --- a/test/functest/simulators/build_sim_image.sh +++ b/test/functest/simulators/build_sim_image.sh @@ -32,6 +32,7 @@ cp $SIMULATORS_DIR/Dockerfile $DOCKER_DIR/. cp -r $OSDF_DIR/osdf $DOCKER_DIR/sim mkdir -p $DOCKER_DIR/sim/config/ cp $SIMULATORS_DIR/simulated-config/*.yaml $DOCKER_DIR/sim/config/ +cp $SIMULATORS_DIR/simulated-config/*.yml $DOCKER_DIR/sim/config/ cp $SIMULATORS_DIR/simulated-config/*.config $DOCKER_DIR/sim/config/ cp -r $SIMULATORS_DIR/configdb $DOCKER_DIR/sim cp -r $SIMULATORS_DIR/has-api $DOCKER_DIR/sim diff --git a/test/placement-tests/request_placement_vfmod.json b/test/placement-tests/request_placement_vfmod.json index e9f1966..4b2b852 100644 --- a/test/placement-tests/request_placement_vfmod.json +++ b/test/placement-tests/request_placement_vfmod.json @@ -1,92 +1,113 @@ { - "name": "de4f04e3-0a65-470b-9d07-8ea6c2fb3e10", - "template": { - "constraints": { - "affinity_vFW_TD": { - "demands": ["vFW-SINK", "vPGN"], - "properties": { - "category": "region", - "qualifier": "same" - }, - "type": "zone" - } - }, - "parameters": { - "service_name": "vFW_TD", - "chosen_region": "RegionOne", - "service_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c", - "customer_long": 2.2, - "REQUIRED_MEM": "", - "customer_lat": 1.1, - "REQUIRED_DISK": "" + "name": "de4f04e3-0a65-470b-9d07-8ea6c2fb3e10", + "files": {}, + "timeout": 1200, + "num_solution": "100", + "template": { + "homing_template_version": "2017-10-10", + "parameters": { + "REQUIRED_MEM": "", + "REQUIRED_DISK": "", + "customer_lat": 1.1, + "customer_long": 2.2, + "service_name": "vFW_TD", + "service_id": "3e8d118c-10ca-4b4b-b3db-089b5e9e6a1c", + "chosen_region": "RegionOne" + }, + "locations": { + "customer_loc": { + "latitude": { + "get_param": "customer_lat" }, - "locations": { - "customer_loc": { - "longitude": { - "get_param": "customer_long" - }, - "latitude": { - "get_param": "customer_lat" - } + "longitude": { + "get_param": "customer_long" + } + } + }, + "demands": { + "vFW-SINK": [ + { + "inventory_provider": "aai", + "inventory_type": "vfmodule", + "service_type": "vFW-SINK-XX", + "service_resource_id": "vFW-SINK-XX", + "filtering_attributes": { + "global-customer-id": { + "get_param": "chosen_customer_id" + }, + "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d", + "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d", + "orchestration-status": [ + "active" + ], + "prov-status": "ACTIVE", + "cloud-region-id": { + "get_param": "chosen_region" + }, + "service_instance_id": { + "get_param": "service_id" } - }, - "demands": { - "vFW-SINK": [{ - "attributes": { - "global-customer-id": { - "get_param": "chosen_customer_id" - }, - "cloud-region-id": { - "get_param": "chosen_region" - }, - "model-version-id": "763731df-84fd-494b-b824-01fc59a5ff2d", - "orchestration-status": ["active"], - "model-invariant-id": "e7227847-dea6-4374-abca-4561b070fe7d", - "service_instance_id": { - "get_param": "service_id" - }, - "prov-status": "ACTIVE" - }, - "inventory_provider": "aai", - "service_resource_id": "vFW-SINK-XX", - "inventory_type": "vfmodule", - "service_type": "vFW-SINK-XX", - "excluded_candidates": [{ - "inventory_type": "vfmodule", - "candidate_id": ["e765d576-8755-4145-8536-0bb6d9b1dc9a"] - }] - }], - "vPGN": [{ - "attributes": { - "global-customer-id": { - "get_param": "chosen_customer_id" - }, - "cloud-region-id": { - "get_param": "chosen_region" - }, - "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b", - "orchestration-status": ["active"], - "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355", - "service_instance_id": { - "get_param": "service_id" - }, - "prov-status": "ACTIVE" - }, - "inventory_provider": "aai", - "service_resource_id": "vPGN-XX", - "unique": "False", - "inventory_type": "vfmodule", - "service_type": "vPGN-XX" - }] - }, - "optimization": { - "minimize": { - "sum": [] + }, + "passthrough_attributes": { + "td-role": "destination" + }, + "excluded_candidates": [ + { + "inventory_type": "vfmodule", + "candidate_id": [ + "e765d576-8755-4145-8536-0bb6d9b1dc9a" + ] } - }, - "homing_template_version": "2017-10-10" + ] + } + ], + "vPGN": [ + { + "inventory_provider": "aai", + "inventory_type": "vfmodule", + "service_type": "vPGN-XX", + "service_resource_id": "vPGN-XX", + "unique": "False", + "filtering_attributes": { + "global-customer-id": { + "get_param": "chosen_customer_id" + }, + "model-invariant-id": "762472ef-5284-4daa-ab32-3e7bee2ec355", + "model-version-id": "e02a7e5c-9d27-4360-ab7c-73bb83b07e3b", + "orchestration-status": [ + "active" + ], + "prov-status": "ACTIVE", + "cloud-region-id": { + "get_param": "chosen_region" + }, + "service_instance_id": { + "get_param": "service_id" + } + }, + "passthrough_attributes": { + "td-role": "anchor" + } + } + ] + }, + "constraints": { + "affinity_vFW_TD": { + "type": "zone", + "demands": [ + "vFW-SINK", + "vPGN" + ], + "properties": { + "category": "region", + "qualifier": "same" + } + } }, - "num_solution": "100", - "files": {}, - "timeout": 1200 -} + "optimization": { + "minimize": { + "sum": [] + } + } + } +}
\ No newline at end of file diff --git a/test/policy-local-files/vnfPolicy_vFW_TD.json b/test/policy-local-files/vnfPolicy_vFW_TD.json index 9a9cbe0..a471a77 100644 --- a/test/policy-local-files/vnfPolicy_vFW_TD.json +++ b/test/policy-local-files/vnfPolicy_vFW_TD.json @@ -35,7 +35,10 @@ "service_instance_id": { "get_param": "service_id" } + }, + "passthroughAttributes": { + "td-role": "destination" } }] } -}
\ No newline at end of file +} diff --git a/test/policy-local-files/vnfPolicy_vPGN_TD.json b/test/policy-local-files/vnfPolicy_vPGN_TD.json index 3724f8a..2e79f2f 100644 --- a/test/policy-local-files/vnfPolicy_vPGN_TD.json +++ b/test/policy-local-files/vnfPolicy_vPGN_TD.json @@ -1,42 +1,51 @@ { - "service": "vnfPolicy", - "policyName": "OSDF_DUBLIN.vnfPolicy_vPGN_TD", - "description": "vnfPolicy", - "templateVersion": "OpenSource.version.1", - "version": "oofDublin", - "priority": "6", - "riskType": "test", - "riskLevel": "3", - "guard": "False", - "content": { - "identity": "vnf_vPGN_TD", - "policyScope": [ - "td", - "us", - "vPGN" - ], - "policyType": "vnfPolicy", - "resources": ["vPGN"], - "applicableResources": "any", - "vnfProperties": [{ - "inventoryProvider": "aai", - "serviceType": "", - "inventoryType": "vfmodule", - "customerId": { - "get_param": "chosen_customer_id" - }, - "equipmentRole": "", - "unique": "False", - "attributes": { - "orchestrationStatus": ["active"], - "provStatus": "ACTIVE", - "cloudRegionId": { - "get_param": "chosen_region" - }, - "service_instance_id": { - "get_param": "service_id" - } - } - }] - } + "service": "vnfPolicy", + "policyName": "OSDF_DUBLIN.vnfPolicy_vPGN_TD", + "description": "vnfPolicy", + "templateVersion": "OpenSource.version.1", + "version": "oofDublin", + "priority": "6", + "riskType": "test", + "riskLevel": "3", + "guard": "False", + "content": { + "identity": "vnf_vPGN_TD", + "policyScope": [ + "td", + "us", + "vPGN" + ], + "policyType": "vnfPolicy", + "resources": [ + "vPGN" + ], + "applicableResources": "any", + "vnfProperties": [ + { + "inventoryProvider": "aai", + "serviceType": "", + "inventoryType": "vfmodule", + "customerId": { + "get_param": "chosen_customer_id" + }, + "equipmentRole": "", + "unique": "False", + "attributes": { + "orchestrationStatus": [ + "active" + ], + "provStatus": "ACTIVE", + "cloudRegionId": { + "get_param": "chosen_region" + }, + "service_instance_id": { + "get_param": "service_id" + } + }, + "passthroughAttributes": { + "td-role": "anchor" + } + } + ] + } }
\ No newline at end of file diff --git a/test/test-requirements.txt b/test/test-requirements.txt index 043d87a..c8d5613 100644 --- a/test/test-requirements.txt +++ b/test/test-requirements.txt @@ -3,3 +3,4 @@ moto pytest pytest-tap requests-mock +pylint diff --git a/test/test_ConductorApiBuilder.py b/test/test_ConductorApiBuilder.py index e69e954..07cb3bb 100644 --- a/test/test_ConductorApiBuilder.py +++ b/test/test_ConductorApiBuilder.py @@ -47,7 +47,7 @@ class TestConductorApiBuilder(unittest.TestCase): main_dir = self.main_dir request_json = self.request_json policies = self.policies - local_config = yaml.load(open(self.local_config_file)) + local_config = yaml.safe_load(open(self.local_config_file)) templ_string = conductor_api_builder(request_json, policies, local_config, self.conductor_api_template) templ_json = json.loads(templ_string) self.assertEqual(templ_json["name"], "yyy-yyy-yyyy") @@ -55,7 +55,7 @@ class TestConductorApiBuilder(unittest.TestCase): def test_conductor_api_call_builder_vfmod(self): request_json = self.request_vfmod_json policies = self.policies - local_config = yaml.load(open(self.local_config_file)) + local_config = yaml.safe_load(open(self.local_config_file)) templ_string = conductor_api_builder(request_json, policies, local_config, self.conductor_api_template) templ_json = json.loads(templ_string) self.assertEqual(templ_json, self.request_placement_vfmod_json) @@ -5,6 +5,7 @@ envlist = py3, pylint [testenv] distribute = False +basepython=python3 setenv = OSDF_CONFIG_FILE={toxinidir}/test/config/osdf_config.yaml commands = @@ -18,11 +19,11 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test/test-requirements.txt [run] -source=./osdf/,osdfapp.py +source=./apps/,./osdf/,osdfapp.py [testenv:pylint] whitelist_externals=bash -commands = bash -c "pylint --reports=y osdf | tee pylint.out" +commands = bash -c "pylint --reports=y osdf apps| tee pylint.out" [testenv:py3] basepython=python3.6 |