diff --git a/spyglass/data_extractor/base.py b/spyglass/data_extractor/base.py index 955e7f8..c7088d7 100644 --- a/spyglass/data_extractor/base.py +++ b/spyglass/data_extractor/base.py @@ -1,21 +1,21 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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 abc +import logging import pprint import six -import logging from spyglass.utils import utils @@ -34,8 +34,7 @@ class BaseDataSourcePlugin(object): @abc.abstractmethod def set_config_opts(self, conf): - """Placeholder to set configuration options - specific to each plugin. + """Placeholder to set configuration options specific to each plugin. :param dict conf: Configuration options as dict @@ -44,19 +43,21 @@ class BaseDataSourcePlugin(object): Each plugin will have their own config opts. """ + return @abc.abstractmethod def get_plugin_conf(self, kwargs): - """ Validate and returns the plugin config parameters. + """Validate and returns the plugin config parameters. + If validation fails, Spyglass exits. :param char pointer: Spyglass CLI parameters. - :returns plugin conf if successfully validated. Each plugin implements their own validaton mechanism. """ + return {} @abc.abstractmethod @@ -64,13 +65,12 @@ class BaseDataSourcePlugin(object): """Return list of racks in the region :param string region: Region name - :returns: list of rack names - :rtype: list Example: ['rack01', 'rack02'] """ + return [] @abc.abstractmethod @@ -79,9 +79,7 @@ class BaseDataSourcePlugin(object): :param string region: Region name :param string rack: Rack name - :returns: list of hosts information - :rtype: list of dict Example: [ @@ -96,6 +94,7 @@ class BaseDataSourcePlugin(object): 'host_profile': 'hp_02'} ] """ + return [] @abc.abstractmethod @@ -103,9 +102,7 @@ class BaseDataSourcePlugin(object): """Return list of networks in the region :param string region: Region name - :returns: list of networks and their vlans - :rtype: list of dict Example: [ @@ -158,9 +155,7 @@ class BaseDataSourcePlugin(object): :param string region: Region name :param string host: Host name - :returns: Dict of IPs per network on the host - :rtype: dict Example: {'oob': {'ipv4': '192.168.1.10'}, @@ -170,6 +165,7 @@ class BaseDataSourcePlugin(object): dict. In case some networks are missed, they are expected to be either DHCP or internally generated n the next steps by the design rules. """ + return {} @abc.abstractmethod @@ -177,13 +173,12 @@ class BaseDataSourcePlugin(object): """Return the DNS servers :param string region: Region name - :returns: List of DNS servers to be configured on host - :rtype: List Example: ['8.8.8.8', '8.8.8.4'] """ + return [] @abc.abstractmethod @@ -191,13 +186,12 @@ class BaseDataSourcePlugin(object): """Return the NTP servers :param string region: Region name - :returns: List of NTP servers to be configured on host - :rtype: List Example: ['ntp1.ubuntu1.example', 'ntp2.ubuntu.example'] """ + return [] @abc.abstractmethod @@ -205,9 +199,7 @@ class BaseDataSourcePlugin(object): """Return the LDAP server information :param string region: Region name - :returns: LDAP server information - :rtype: Dict Example: {'url': 'ldap.example.com', @@ -215,6 +207,7 @@ class BaseDataSourcePlugin(object): 'domain': 'test', 'subdomain': 'test_sub1'} """ + return {} @abc.abstractmethod @@ -222,9 +215,7 @@ class BaseDataSourcePlugin(object): """Return location information :param string region: Region name - :returns: Dict of location information - :rtype: dict Example: {'name': 'Dallas', @@ -233,6 +224,7 @@ class BaseDataSourcePlugin(object): 'country': 'US', 'corridor': 'CR1'} """ + return {} @abc.abstractmethod @@ -240,20 +232,18 @@ class BaseDataSourcePlugin(object): """Return the Domain name :param string region: Region name - :returns: Domain name - :rtype: string Example: example.com """ + return "" def extract_baremetal_information(self): """Get baremetal information from plugin :returns: dict of baremetal nodes - :rtype: dict Return dict should be in the format @@ -275,6 +265,7 @@ class BaseDataSourcePlugin(object): } } """ + LOG.info("Extract baremetal information from plugin") baremetal = {} hosts = self.get_hosts(self.region) @@ -330,7 +321,6 @@ class BaseDataSourcePlugin(object): """Get site information from plugin :returns: dict of site information - :rtpe: dict Return dict should be in the format @@ -346,6 +336,7 @@ class BaseDataSourcePlugin(object): 'domain': None } """ + LOG.info("Extract site information from plugin") site_info = {} @@ -373,11 +364,9 @@ class BaseDataSourcePlugin(object): return site_info def extract_network_information(self): - """Get network information from plugin - like Subnets, DNS, NTP, LDAP details. + """Get network details from plugin like Subnets, DNS, NTP and LDAP :returns: dict of baremetal nodes - :rtype: dict Return dict should be in the format @@ -393,6 +382,7 @@ class BaseDataSourcePlugin(object): } } """ + LOG.info("Extract network information from plugin") network_data = {} networks = self.get_networks(self.region) @@ -431,6 +421,7 @@ class BaseDataSourcePlugin(object): Gather data related to baremetal, networks, storage and other site related information from plugin """ + LOG.info("Extract data from plugin") site_data = {} site_data["baremetal"] = self.extract_baremetal_information() @@ -448,6 +439,7 @@ class BaseDataSourcePlugin(object): If there is repetition of data then additional data supplied shall take precedence. """ + LOG.info("Update site data with additional input") tmp_site_data = utils.dict_merge(self.site_data, extra_data) self.site_data = tmp_site_data diff --git a/spyglass/data_extractor/custom_exceptions.py b/spyglass/data_extractor/custom_exceptions.py index 44b0536..c5a8604 100644 --- a/spyglass/data_extractor/custom_exceptions.py +++ b/spyglass/data_extractor/custom_exceptions.py @@ -1,16 +1,17 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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 logging import sys diff --git a/spyglass/data_extractor/plugins/formation.py b/spyglass/data_extractor/plugins/formation.py index 30d4404..ce95e49 100644 --- a/spyglass/data_extractor/plugins/formation.py +++ b/spyglass/data_extractor/plugins/formation.py @@ -1,32 +1,27 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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 formation_client import logging import pprint import re import requests -import formation_client import urllib3 from spyglass.data_extractor.base import BaseDataSourcePlugin -from spyglass.data_extractor.custom_exceptions import ( - ApiClientError, - ConnectionError, - MissingAttributeError, - TokenGenerationError, -) +import spyglass.data_extractor.custom_exceptions as exceptions urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -65,7 +60,8 @@ class FormationPlugin(BaseDataSourcePlugin): LOG.info("Initiated data extractor plugin:{}".format(self.source_name)) def set_config_opts(self, conf): - """ Sets the config params passed by CLI""" + """Sets the config params passed by CLI""" + LOG.info("Plugin params passed:\n{}".format(pprint.pformat(conf))) self._validate_config_options(conf) self.formation_api_url = conf["url"] @@ -77,7 +73,7 @@ class FormationPlugin(BaseDataSourcePlugin): self._update_site_and_zone(self.region) def get_plugin_conf(self, kwargs): - """ Validates the plugin param and return if success""" + """Validates the plugin param and return if success""" if not kwargs["formation_url"]: LOG.error("formation_url not specified! Spyglass exited!") @@ -116,10 +112,12 @@ class FormationPlugin(BaseDataSourcePlugin): def _generate_token(self): """Generate token for Formation + Formation API does not provide separate resource to generate token. This is a workaround to call directly Formation API to get token instead of using Formation client. """ + # Create formation client config object self.client_config = formation_client.Configuration() self.client_config.host = self.formation_api_url @@ -138,13 +136,13 @@ class FormationPlugin(BaseDataSourcePlugin): auth=(self.user, self.password), verify=self.client_config.verify_ssl, ) - except requests.exceptions.ConnectionError: - raise ConnectionError("Incorrect URL: {}".format(url)) + except requests.exceptions.exceptions.ConnectionError: + raise exceptions.ConnectionError("Incorrect URL: {}".format(url)) if token_response.status_code == 200: self.token = token_response.json().get("X-Subject-Token", None) else: - raise TokenGenerationError( + raise exceptions.TokenGenerationError( "Unable to generate token because {}".format( token_response.reason ) @@ -159,6 +157,7 @@ class FormationPlugin(BaseDataSourcePlugin): format "user|token". Generate the token and add it formation config object. """ + token = self._generate_token() self.client_config.api_key = {"X-Auth-Token": self.user + "|" + token} self.formation_api_client = formation_client.ApiClient( @@ -428,7 +427,7 @@ class FormationPlugin(BaseDataSourcePlugin): return ip_ def _get_network_name_from_vlan_name(self, vlan_name): - """ network names are ksn, oam, oob, overlay, storage, pxe + """Network names are ksn, oam, oob, overlay, storage, pxe The following mapping rules apply: vlan_name contains "ksn" the network name is "calico" @@ -437,6 +436,7 @@ class FormationPlugin(BaseDataSourcePlugin): vlan_name contains "ovs" the network name is "overlay" vlan_name contains "ILO" the network name is "oob" """ + network_names = { "ksn": "calico", "storage": "storage", @@ -462,7 +462,7 @@ class FormationPlugin(BaseDataSourcePlugin): zone_api = formation_client.ZonesApi(self.formation_api_client) zone_ = zone_api.zones_zone_id_get(zone_id) except formation_client.rest.ApiException as e: - raise ApiClientError(e.msg) + raise exceptions.ApiClientError(e.msg) if not zone_.ipv4_dns: LOG.warn("No dns server") @@ -481,7 +481,8 @@ class FormationPlugin(BaseDataSourcePlugin): return {} def get_location_information(self, region): - """ get location information for a zone and return """ + """Get location information for a zone and return""" + site = self.region_zone_map[region]["site"] site_id = self._get_site_id_by_name(site) site_api = formation_client.SitesApi(self.formation_api_client) @@ -496,7 +497,7 @@ class FormationPlugin(BaseDataSourcePlugin): "physical_location_id": site_info.clli, } except AttributeError as e: - raise MissingAttributeError( + raise exceptions.MissingAttributeError( "Missing {} information in {}".format(e, site_info.city) ) @@ -507,7 +508,7 @@ class FormationPlugin(BaseDataSourcePlugin): zone_api = formation_client.ZonesApi(self.formation_api_client) zone_ = zone_api.zones_zone_id_get(zone_id) except formation_client.rest.ApiException as e: - raise ApiClientError(e.msg) + raise exceptions.ApiClientError(e.msg) if not zone_.dns: LOG.warn("Got None while running get domain name") diff --git a/spyglass/data_extractor/plugins/tugboat/check_exceptions.py b/spyglass/data_extractor/plugins/tugboat/check_exceptions.py index a077af4..129f6c5 100644 --- a/spyglass/data_extractor/plugins/tugboat/check_exceptions.py +++ b/spyglass/data_extractor/plugins/tugboat/check_exceptions.py @@ -4,7 +4,7 @@ # 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 +# 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, diff --git a/spyglass/data_extractor/plugins/tugboat/excel_parser.py b/spyglass/data_extractor/plugins/tugboat/excel_parser.py index 47559f5..dc05085 100644 --- a/spyglass/data_extractor/plugins/tugboat/excel_parser.py +++ b/spyglass/data_extractor/plugins/tugboat/excel_parser.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -13,12 +13,13 @@ # limitations under the License. import logging +from openpyxl import load_workbook +from openpyxl import Workbook import pprint import re import sys import yaml -from openpyxl import load_workbook -from openpyxl import Workbook + from spyglass.data_extractor.custom_exceptions import NoSpecMatched # from spyglass.data_extractor.custom_exceptions @@ -26,8 +27,8 @@ from spyglass.data_extractor.custom_exceptions import NoSpecMatched LOG = logging.getLogger(__name__) -class ExcelParser: - """ Parse data from excel into a dict """ +class ExcelParser(object): + """Parse data from excel into a dict""" def __init__(self, file_name, excel_specs): self.file_name = file_name @@ -43,15 +44,18 @@ class ExcelParser: @staticmethod def sanitize(string): - """ Remove extra spaces and convert string to lower case """ + """Remove extra spaces and convert string to lower case""" + return string.replace(" ", "").lower() def compare(self, string1, string2): - """ Compare the strings """ + """Compare the strings""" + return bool(re.search(self.sanitize(string1), self.sanitize(string2))) def validate_sheet(self, spec, sheet): - """ Check if the sheet is correct or not """ + """Check if the sheet is correct or not""" + ws = self.wb_combined[sheet] header_row = self.excel_specs["specs"][spec]["header_row"] ipmi_header = self.excel_specs["specs"][spec]["ipmi_address_header"] @@ -60,7 +64,8 @@ class ExcelParser: return bool(self.compare(ipmi_header, header_value)) def find_correct_spec(self): - """ Find the correct spec """ + """Find the correct spec""" + for spec in self.excel_specs["specs"]: sheet_name = self.excel_specs["specs"][spec]["ipmi_sheet_name"] for sheet in self.wb_combined.sheetnames: @@ -71,7 +76,8 @@ class ExcelParser: raise NoSpecMatched(self.excel_specs) def get_ipmi_data(self): - """ Read IPMI data from the sheet """ + """Read IPMI data from the sheet""" + ipmi_data = {} hosts = [] provided_sheetname = self.excel_specs["specs"][self.spec][ @@ -140,7 +146,8 @@ class ExcelParser: return [ipmi_data, hosts] def get_private_vlan_data(self, ws): - """ Get private vlan data from private IP sheet """ + """Get private vlan data from private IP sheet""" + vlan_data = {} row = self.excel_specs["specs"][self.spec]["vlan_start_row"] end_row = self.excel_specs["specs"][self.spec]["vlan_end_row"] @@ -160,7 +167,8 @@ class ExcelParser: return vlan_data def get_private_network_data(self): - """ Read network data from the private ip sheet """ + """Read network data from the private ip sheet""" + provided_sheetname = self.excel_specs["specs"][self.spec][ "private_ip_sheet" ] @@ -211,7 +219,8 @@ class ExcelParser: return network_data def get_public_network_data(self): - """ Read public network data from public ip data """ + """Read public network data from public ip data""" + network_data = {} provided_sheetname = self.excel_specs["specs"][self.spec][ "public_ip_sheet" @@ -251,7 +260,8 @@ class ExcelParser: return network_data def get_site_info(self): - """ Read location, dns, ntp and ldap data""" + """Read location, dns, ntp and ldap data""" + site_info = {} provided_sheetname = self.excel_specs["specs"][self.spec][ "dns_ntp_ldap_sheet" @@ -326,7 +336,8 @@ class ExcelParser: return site_info def get_location_data(self): - """ Read location data from the site and zone sheet """ + """Read location data from the site and zone sheet""" + provided_sheetname = self.excel_specs["specs"][self.spec][ "location_sheet" ] @@ -356,7 +367,8 @@ class ExcelParser: } def validate_sheet_names_with_spec(self): - """ Checks is sheet name in spec file matches with excel file""" + """Checks is sheet name in spec file matches with excel file""" + spec = list(self.excel_specs["specs"].keys())[0] spec_item = self.excel_specs["specs"][spec] sheet_name_list = [] @@ -391,7 +403,8 @@ class ExcelParser: LOG.info("Sheet names in excel spec validated") def get_data(self): - """ Create a dict with combined data """ + """Create a dict with combined data""" + self.validate_sheet_names_with_spec() ipmi_data = self.get_ipmi_data() network_data = self.get_private_network_data() @@ -413,7 +426,8 @@ class ExcelParser: return data def combine_excel_design_specs(self, filenames): - """ Combines multiple excel file to a single design spec""" + """Combines multiple excel file to a single design spec""" + design_spec = Workbook() for exel_file in filenames: loaded_workbook = load_workbook(exel_file, data_only=True) @@ -428,10 +442,11 @@ class ExcelParser: return design_spec def get_xl_obj_and_sheetname(self, sheetname): + """The logic confirms if the sheetname is specified for example as: + + 'MTN57a_AEC_Network_Design_v1.6.xlsx:Public IPs' """ - The logic confirms if the sheetname is specified for example as: - "MTN57a_AEC_Network_Design_v1.6.xlsx:Public IPs" - """ + if re.search(".xlsx", sheetname) or re.search(".xls", sheetname): """ Extract file name """ source_xl_file = sheetname.split(":")[0] diff --git a/spyglass/data_extractor/plugins/tugboat/tugboat.py b/spyglass/data_extractor/plugins/tugboat/tugboat.py index 495363d..461d7d2 100644 --- a/spyglass/data_extractor/plugins/tugboat/tugboat.py +++ b/spyglass/data_extractor/plugins/tugboat/tugboat.py @@ -1,13 +1,13 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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. @@ -41,9 +41,7 @@ class TugboatPlugin(BaseDataSourcePlugin): LOG.info("Initiated data extractor plugin:{}".format(self.source_name)) def set_config_opts(self, conf): - """ - Placeholder to set configuration options - specific to each plugin. + """Placeholder to set configuration options specific to each plugin. :param dict conf: Configuration options as dict @@ -52,6 +50,7 @@ class TugboatPlugin(BaseDataSourcePlugin): Each plugin will have their own config opts. """ + self.excel_path = conf["excel_path"] self.excel_spec = conf["excel_spec"] @@ -61,8 +60,7 @@ class TugboatPlugin(BaseDataSourcePlugin): return def get_plugin_conf(self, kwargs): - """ Validates the plugin param from CLI and return if correct - + """Validates the plugin param from CLI and return if correct Ideally the CLICK module shall report an error if excel file and excel specs are not specified. The below code has been @@ -84,6 +82,7 @@ class TugboatPlugin(BaseDataSourcePlugin): def get_hosts(self, region, rack=None): """Return list of hosts in the region + :param string region: Region name :param string rack: Rack name :returns: list of hosts information @@ -100,6 +99,7 @@ class TugboatPlugin(BaseDataSourcePlugin): 'host_profile': 'hp_02'} ] """ + LOG.info("Get Host Information") ipmi_data = self.parsed_xl_data["ipmi_data"][0] rackwise_hosts = self._get_rackwise_hosts() @@ -116,7 +116,8 @@ class TugboatPlugin(BaseDataSourcePlugin): return host_list def get_networks(self, region): - """ Extracts vlan network info from raw network data from excel""" + """Extracts vlan network info from raw network data from excel""" + vlan_list = [] # Network data extracted from xl is formatted to have a predictable # data type. For e.g VlAN 45 extracted from xl is formatted as 45 @@ -162,6 +163,7 @@ class TugboatPlugin(BaseDataSourcePlugin): def get_ips(self, region, host=None): """Return list of IPs on the host + :param string region: Region name :param string host: Host name :returns: Dict of IPs per network on the host @@ -186,7 +188,7 @@ class TugboatPlugin(BaseDataSourcePlugin): return ip_ def get_ldap_information(self, region): - """ Extract ldap information from excel""" + """Extract ldap information from excel""" ldap_raw_data = self.parsed_xl_data["site_info"]["ldap"] ldap_info = {} @@ -206,7 +208,7 @@ class TugboatPlugin(BaseDataSourcePlugin): return ldap_info def get_ntp_servers(self, region): - """ Returns a comma separated list of ntp ip addresses""" + """Returns a comma separated list of ntp ip addresses""" ntp_server_list = self._get_formatted_server_list( self.parsed_xl_data["site_info"]["ntp"] @@ -214,22 +216,23 @@ class TugboatPlugin(BaseDataSourcePlugin): return ntp_server_list def get_dns_servers(self, region): - """ Returns a comma separated list of dns ip addresses""" + """Returns a comma separated list of dns ip addresses""" dns_server_list = self._get_formatted_server_list( self.parsed_xl_data["site_info"]["dns"] ) return dns_server_list def get_domain_name(self, region): - """ Returns domain name extracted from excel file""" + """Returns domain name extracted from excel file""" return self.parsed_xl_data["site_info"]["domain"] def get_location_information(self, region): - """ - Prepare location data from information extracted + """Prepare location data from information extracted + by ExcelParser(i.e raw data) """ + location_data = self.parsed_xl_data["site_info"]["location"] corridor_pattern = r"\d+" @@ -255,20 +258,21 @@ class TugboatPlugin(BaseDataSourcePlugin): pass def _get_excel_obj(self): - """ Creation of an ExcelParser object to store site information. + """Creation of an ExcelParser object to store site information. The information is obtained based on a excel spec yaml file. This spec contains row, column and sheet information of the excel file from where site specific data can be extracted. """ + self.excel_obj = ExcelParser(self.excel_path, self.excel_spec) def _extract_raw_data_from_excel(self): - """ Extracts raw information from excel file based on excel spec""" + """Extracts raw information from excel file based on excel spec""" self.parsed_xl_data = self.excel_obj.get_data() def _get_network_name_from_vlan_name(self, vlan_name): - """ network names are ksn, oam, oob, overlay, storage, pxe + """Network names are ksn, oam, oob, overlay, storage, pxe This is a utility function to determine the vlan acceptable @@ -282,6 +286,7 @@ class TugboatPlugin(BaseDataSourcePlugin): vlan_name contains "oob" the network name is "oob" vlan_name contains "pxe" the network name is "pxe" """ + network_names = [ "ksn|calico", "storage", @@ -314,7 +319,7 @@ class TugboatPlugin(BaseDataSourcePlugin): return "" def _get_formatted_server_list(self, server_list): - """ Format dns and ntp server list as comma separated string """ + """Format dns and ntp server list as comma separated string""" # dns/ntp server info from excel is of the format # 'xxx.xxx.xxx.xxx, (aaa.bbb.ccc.com)' @@ -327,10 +332,8 @@ class TugboatPlugin(BaseDataSourcePlugin): return formatted_server_list def _get_rack(self, host): - """ - Get rack id from the rack string extracted - from xl - """ + """Get rack id from the rack string extracted from xl""" + rack_pattern = r"\w.*(r\d+)\w.*" rack = re.findall(rack_pattern, host)[0] if not self.region: @@ -338,7 +341,8 @@ class TugboatPlugin(BaseDataSourcePlugin): return rack def _get_rackwise_hosts(self): - """ Mapping hosts with rack ids """ + """Mapping hosts with rack ids""" + rackwise_hosts = {} hostnames = self.parsed_xl_data["ipmi_data"][1] racks = self._get_rack_data() @@ -352,7 +356,8 @@ class TugboatPlugin(BaseDataSourcePlugin): return rackwise_hosts def _get_rack_data(self): - """ Format rack name """ + """Format rack name""" + LOG.info("Getting rack data") racks = {} hostnames = self.parsed_xl_data["ipmi_data"][1] diff --git a/spyglass/examples/excel_spec.yaml b/spyglass/examples/excel_spec.yaml index 62831a0..694738e 100644 --- a/spyglass/examples/excel_spec.yaml +++ b/spyglass/examples/excel_spec.yaml @@ -29,7 +29,7 @@ specs: private_ip_sheet: 'Site-Information' net_type_col: 1 vlan_col: 2 - vlan_start_row: 19 + vlan_start_row: 19 vlan_end_row: 30 net_start_row: 33 net_end_row: 40 @@ -56,7 +56,7 @@ specs: domain_col: 2 location_sheet: 'Site-Information' column: 2 - corridor_row: 59 + corridor_row: 59 site_name_row: 58 state_name_row: 60 country_name_row: 61 diff --git a/spyglass/parser/engine.py b/spyglass/parser/engine.py index 95ab944..c2b945d 100644 --- a/spyglass/parser/engine.py +++ b/spyglass/parser/engine.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -26,7 +26,7 @@ import yaml LOG = logging.getLogger(__name__) -class ProcessDataSource: +class ProcessDataSource(object): def __init__(self, sitetype): # Initialize intermediary and save site type self._initialize_intermediary() @@ -53,11 +53,12 @@ class ProcessDataSource: self.network_subnets = None def _get_network_subnets(self): - """ Extract subnet information for networks. - + """Extract subnet information for networks. In some networks, there are multiple subnets, in that case - we assign only the first subnet """ + we assign only the first subnet + """ + LOG.info("Extracting network subnets") network_subnets = {} for net_type in self.data["network"]["vlan_network_data"]: @@ -89,7 +90,8 @@ class ProcessDataSource: ) def _get_genesis_node_ip(self): - """ Returns the genesis node ip """ + """Returns the genesis node ip""" + ip = "0.0.0.0" LOG.info("Getting Genesis Node IP") if not self.genesis_node: @@ -100,13 +102,13 @@ class ProcessDataSource: return ip def _validate_intermediary_data(self, data): - """ Validates the intermediary data before generating manifests. - + """Validates the intermediary data before generating manifests. It checks wether the data types and data format are as expected. The method validates this with regex pattern defined for each data type. """ + LOG.info("Validating Intermediary data") temp_data = {} # Peforming a deep copy @@ -147,14 +149,14 @@ class ProcessDataSource: LOG.info("Data validation Passed!") def _apply_design_rules(self): - """ Applies design rules from rules.yaml - + """Applies design rules from rules.yaml These rules are used to determine ip address allocation ranges, host profile interfaces and also to create hardware profile information. The method calls corresponding rule hander function based on rule name and applies them to appropriate data objects. """ + LOG.info("Apply design rules") rules_dir = pkg_resources.resource_filename("spyglass", "config/") rules_file = rules_dir + "rules.yaml" @@ -178,13 +180,14 @@ class ProcessDataSource: pass def _apply_rule_hardware_profile(self, rule_data): - """ Apply rules to define host type from hardware profile info. - + """Apply rules to define host type from hardware profile info. Host profile will define host types as "controller, compute or genesis". The rule_data has pre-defined information to define compute or controller based on host_profile. For defining 'genesis' - the first controller host is defined as genesis.""" + the first controller host is defined as genesis. + """ + is_genesis = False hardware_profile = rule_data[self.data["site_info"]["sitetype"]] # Getting individual racks. The racks are sorted to ensure that the @@ -207,8 +210,10 @@ class ProcessDataSource: host_info["type"] = "compute" def _apply_rule_ip_alloc_offset(self, rule_data): - """ Apply offset rules to update baremetal host ip's and vlan network - data """ + """Apply offset rules to update baremetal host + + ip's and vlan network + """ # Get network subnets self.network_subnets = self._get_network_subnets() @@ -217,12 +222,12 @@ class ProcessDataSource: self._update_baremetal_host_ip_data(rule_data) def _update_baremetal_host_ip_data(self, rule_data): - """ Update baremetal host ip's for applicable networks. - + """Update baremetal host ip's for applicable networks. The applicable networks are oob, oam, ksn, storage and overlay. These IPs are assigned based on network subnets ranges. - If a particular ip exists it is overridden.""" + If a particular ip exists it is overridden. + """ # Ger defult ip offset default_ip_offset = rule_data["default"] @@ -245,12 +250,12 @@ class ProcessDataSource: ) def _update_vlan_net_data(self, rule_data): - """ Offset allocation rules to determine ip address range(s) - + """Offset allocation rules to determine ip address range(s) This rule is applied to incoming network data to determine network address, gateway ip and other address ranges """ + LOG.info("Apply network design rules") # Collect Rules @@ -348,10 +353,11 @@ class ProcessDataSource: ) def load_extracted_data_from_data_source(self, extracted_data): - """ - Function called from spyglass.py to pass extracted data + """Function called from spyglass.py to pass extracted data + from input data source """ + # TBR(pg710r): for internal testing """ raw_data = self._read_file('extracted_data.yaml') @@ -376,7 +382,8 @@ class ProcessDataSource: self.data["region_name"] = self.region_name def dump_intermediary_file(self, intermediary_dir): - """ Writing intermediary yaml """ + """Writing intermediary yaml""" + LOG.info("Writing intermediary yaml") intermediary_file = "{}_intermediary.yaml".format( self.data["region_name"] @@ -393,7 +400,8 @@ class ProcessDataSource: f.close() def generate_intermediary_yaml(self): - """ Generating intermediary yaml """ + """Generating intermediary yaml""" + LOG.info("Start: Generate Intermediary") self._apply_design_rules() self._get_genesis_node_details() diff --git a/spyglass/site_processors/base.py b/spyglass/site_processors/base.py index bfc048a..44b8daf 100644 --- a/spyglass/site_processors/base.py +++ b/spyglass/site_processors/base.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -13,7 +13,7 @@ # limitations under the License. -class BaseProcessor: +class BaseProcessor(object): def __init__(self, file_name): pass diff --git a/spyglass/site_processors/site_processor.py b/spyglass/site_processors/site_processor.py index 3358773..4bf6bed 100644 --- a/spyglass/site_processors/site_processor.py +++ b/spyglass/site_processors/site_processor.py @@ -4,7 +4,7 @@ # 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 +# 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, @@ -12,11 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging -import os from jinja2 import Environment from jinja2 import FileSystemLoader -from .base import BaseProcessor +import logging +import os +from spyglass.site_processors.base import BaseProcessor LOG = logging.getLogger(__name__) @@ -27,7 +27,7 @@ class SiteProcessor(BaseProcessor): self.manifest_dir = manifest_dir def render_template(self, template_dir): - """ The method renders network config yaml from j2 templates. + """The method renders network config yaml from j2 templates. Network configs common to all racks (i.e oam, overlay, storage, diff --git a/spyglass/spyglass.py b/spyglass/spyglass.py index 7a3733e..8a15015 100644 --- a/spyglass/spyglass.py +++ b/spyglass/spyglass.py @@ -1,13 +1,13 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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/spyglass/utils/utils.py b/spyglass/utils/utils.py index 508e243..bb541de 100644 --- a/spyglass/utils/utils.py +++ b/spyglass/utils/utils.py @@ -1,13 +1,13 @@ # Copyright 2018 AT&T Intellectual Property. All other rights reserved. # -# Licensed under the Apache License, Version 2.0 (the 'License'); +# 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 +# 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, +# 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. @@ -15,7 +15,7 @@ # Merge two dictionaries def dict_merge(dictA, dictB, path=None): - """ Recursively Merge dictionary dictB into dictA + """Recursively Merge dictionary dictB into dictA DictA represents the data extracted by a plugin and DictB diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..42ed909 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,8 @@ +# Formatting +yapf==0.20.0 + +# Linting +hacking>=1.1.0,<1.2.0 # Apache-2.0 + +# Security +bandit>=1.5.0 diff --git a/tools/gate/whitespace-linter.sh b/tools/gate/whitespace-linter.sh new file mode 100755 index 0000000..55f062d --- /dev/null +++ b/tools/gate/whitespace-linter.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -x + +RES=$(git grep -E -l " +$") + +if [[ -n $RES ]]; then + exit 1 +fi diff --git a/tox.ini b/tox.ini index f8a7ffd..4ba6475 100644 --- a/tox.ini +++ b/tox.ini @@ -1,30 +1,40 @@ [tox] -envlist = py35, py36, pep8, docs +envlist = pep8, docs +minversion = 2.3.1 skipsdist = True [testenv] +usedevelop = True +setenv = + VIRTUAL_ENV={envdir} + LANGUAGE=en_US + LC_ALL=en_US.utf-8 deps = -r{toxinidir}/requirements.txt -basepython=python3 + -r{toxinidir}/test-requirements.txt +passenv = http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY PBR_VERSION whitelist_externals = find commands = find . -type f -name "*.pyc" -delete - pytest \ - {posargs} + {toxinidir}/tools/gate/run-unit-tests.sh '{posargs}' [testenv:fmt] -deps = yapf +basepython = python3 +deps = + -r{toxinidir}/test-requirements.txt commands = - yapf --style=pep8 -ir {toxinidir}/spyglass {toxinidir}/tests + yapf -ir {toxinidir}/spyglass {toxinidir}/tests [testenv:pep8] -deps = - yapf - flake8 +basepython = python3 +deps = + -r{toxinidir}/test-requirements.txt commands = - #yapf --style=.style.yapf -rd {toxinidir}/spyglass - flake8 {toxinidir}/spyglass + bash -c "{toxinidir}/tools/gate/whitespace-linter.sh" + flake8 {toxinidir}/spyglass +whitelist_externals = + bash [testenv:bandit] deps =