From 4b269fba0ca273dfa3acf44c9f5490f01e0c3d87 Mon Sep 17 00:00:00 2001 From: Trevor Bramwell Date: Fri, 22 Sep 2017 12:23:36 -0700 Subject: Rename pharos-dashboard and pharos-validator As subdirectories of the pharos-tools repo, there is little need to keep the pharos prefix. Change-Id: Ica3d79411f409df638647300036c0664183c2725 Signed-off-by: Trevor Bramwell --- validator/src/validation_tool/.gitignore | 2 + validator/src/validation_tool/LICENSE | 202 +++++++++++++++++++ .../src/validation_tool/bin/pharos-validator-node | 92 +++++++++ .../validation_tool/bin/pharos-validator-server | 183 +++++++++++++++++ validator/src/validation_tool/doc/config.yaml | 37 ++++ .../src/validation_tool/doc/example/config.yaml | 37 ++++ .../src/validation_tool/doc/example/inventory.yaml | 38 ++++ .../src/validation_tool/doc/example/network.yaml | 207 +++++++++++++++++++ validator/src/validation_tool/doc/inventory.yaml | 38 ++++ validator/src/validation_tool/doc/network.yaml | 221 +++++++++++++++++++++ .../src/validation_tool/doc/pharos-validator.1 | 54 +++++ validator/src/validation_tool/requirements.txt | 3 + validator/src/validation_tool/setup.py | 31 +++ validator/src/validation_tool/src/__init__.py | 0 validator/src/validation_tool/src/config.py | 176 ++++++++++++++++ validator/src/validation_tool/src/const.py | 48 +++++ validator/src/validation_tool/src/dhcp.py | 102 ++++++++++ validator/src/validation_tool/src/ipmi.py | 63 ++++++ validator/src/validation_tool/src/jenkins.py | 8 + validator/src/validation_tool/src/node.py | 85 ++++++++ validator/src/validation_tool/src/receiver.py | 46 +++++ validator/src/validation_tool/src/server.py | 111 +++++++++++ validator/src/validation_tool/src/test/__init__.py | 0 validator/src/validation_tool/src/test/evaluate.py | 159 +++++++++++++++ validator/src/validation_tool/src/test/probe.py | 137 +++++++++++++ validator/src/validation_tool/src/util.py | 107 ++++++++++ validator/src/validation_tool/tests/test_node.py | 0 validator/src/validation_tool/tests/test_probe.py | 0 validator/src/validation_tool/tests/test_server.py | 9 + validator/src/validation_tool/tests/test_util.py | 12 ++ 30 files changed, 2208 insertions(+) create mode 100644 validator/src/validation_tool/.gitignore create mode 100644 validator/src/validation_tool/LICENSE create mode 100755 validator/src/validation_tool/bin/pharos-validator-node create mode 100755 validator/src/validation_tool/bin/pharos-validator-server create mode 100644 validator/src/validation_tool/doc/config.yaml create mode 100644 validator/src/validation_tool/doc/example/config.yaml create mode 100644 validator/src/validation_tool/doc/example/inventory.yaml create mode 100644 validator/src/validation_tool/doc/example/network.yaml create mode 100644 validator/src/validation_tool/doc/inventory.yaml create mode 100644 validator/src/validation_tool/doc/network.yaml create mode 100644 validator/src/validation_tool/doc/pharos-validator.1 create mode 100644 validator/src/validation_tool/requirements.txt create mode 100755 validator/src/validation_tool/setup.py create mode 100644 validator/src/validation_tool/src/__init__.py create mode 100644 validator/src/validation_tool/src/config.py create mode 100644 validator/src/validation_tool/src/const.py create mode 100644 validator/src/validation_tool/src/dhcp.py create mode 100644 validator/src/validation_tool/src/ipmi.py create mode 100644 validator/src/validation_tool/src/jenkins.py create mode 100644 validator/src/validation_tool/src/node.py create mode 100644 validator/src/validation_tool/src/receiver.py create mode 100644 validator/src/validation_tool/src/server.py create mode 100644 validator/src/validation_tool/src/test/__init__.py create mode 100644 validator/src/validation_tool/src/test/evaluate.py create mode 100644 validator/src/validation_tool/src/test/probe.py create mode 100644 validator/src/validation_tool/src/util.py create mode 100644 validator/src/validation_tool/tests/test_node.py create mode 100644 validator/src/validation_tool/tests/test_probe.py create mode 100644 validator/src/validation_tool/tests/test_server.py create mode 100644 validator/src/validation_tool/tests/test_util.py (limited to 'validator/src/validation_tool') diff --git a/validator/src/validation_tool/.gitignore b/validator/src/validation_tool/.gitignore new file mode 100644 index 0000000..5559a0a --- /dev/null +++ b/validator/src/validation_tool/.gitignore @@ -0,0 +1,2 @@ +tests/*.xml +build/ diff --git a/validator/src/validation_tool/LICENSE b/validator/src/validation_tool/LICENSE new file mode 100644 index 0000000..7a7c11a --- /dev/null +++ b/validator/src/validation_tool/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Todd Gaunt and others + + 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/validator/src/validation_tool/bin/pharos-validator-node b/validator/src/validation_tool/bin/pharos-validator-node new file mode 100755 index 0000000..e81bc1b --- /dev/null +++ b/validator/src/validation_tool/bin/pharos-validator-node @@ -0,0 +1,92 @@ +#!/usr/bin/env/python3 +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import argparse +import os +import sys +import logging + +from pharosvalidator import node + +def main(): + """Run validation tests on machine, then send results back to server + on jump host""" + args = parse_args() + + logger = configure_root_logger(0) + + if args["test"] == "hardware": + result = node.hardware_test() + elif args["test"] == "network": + result = node.network_test() + else: + logger.error("Invalid test name chosen, please choose \"hardware\" or \"network\"") + quit() + + logger.debug("TEST RESULTS\n" + "#"*50 + '\n' + result + "#"*50 + '\n') + logger.info("Sending results to host...") + node.send_result(args["host"], args["port"], result) + +def configure_root_logger(loglevel): + # Add a file handler to the default logger + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + + # Configure the root logger + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setLevel(loglevel) + stdout_handler.setFormatter(formatter) + + root_logger = logging.getLogger() + root_logger.addHandler(stdout_handler) + root_logger.setLevel(loglevel) + + return root_logger + +def parse_args(): + """ + parse_args: parse the commandline arguments and configuration file into + a dictionary that can be easily passed and referenced by other functions + + input: None + + output: Dictionary of all commandline arguments and configuration file + settings + """ + logger = logging.getLogger(__name__) + + parser = argparse.ArgumentParser( \ + description='evaluates a system against the pharos specification') + + parser.add_argument('--version', + action="store_true", default=False, + help='display version then exit') + + # Address that the client should connect to + parser.add_argument('-H', '--host', + type=str, default="0.0.0.0", + help='Address of the server results should be \ + uploaded to') + + # Port that the client should connect to + parser.add_argument('-p', '--port', + type=str, default=0, + help='Port of the server results will be uploaded to') + + # Specify which test to run on the node + parser.add_argument('test', metavar='test', + type=str, + help='Which test should be run ["hardware", "network"]') + + args = vars(parser.parse_args()) + + return args + +if __name__ == "__main__": + main() diff --git a/validator/src/validation_tool/bin/pharos-validator-server b/validator/src/validation_tool/bin/pharos-validator-server new file mode 100755 index 0000000..ac9e4f8 --- /dev/null +++ b/validator/src/validation_tool/bin/pharos-validator-server @@ -0,0 +1,183 @@ +#!/usr/bin/env/python3 +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import yaml +import threading +import queue +import argparse +import logging +import os +import sys + +from pharosvalidator import receiver +from pharosvalidator import util +from pharosvalidator import dhcp +from pharosvalidator import ipmi +from pharosvalidator import config +from pharosvalidator import server + +def main(): + """PXE boot each nodes, then start up server to receive results""" + # The logger instance for this function + logger = logging.getLogger("pharosvalidator") + args = parse_args() + + # Determine the logging level + loglevel = logging.INFO + if args["verbose"]: + loglevel = logging.DEBUG + if args["quiet"]: + loglevel = logging.CRITICAL + + configure_root_logger(loglevel, args["logfile"]) + + # Create a new logger strictly for logging test results to a file + test_logger = logging.getLogger('test_logger') + test_logger.setLevel(logging.INFO) + tl_handler = logging.FileHandler(args["test_log"]) + tl_handler.setFormatter(logging.Formatter("%(message)s")) + test_logger.addHandler(tl_handler) + + # Open up the inventory file + invconf = config.Inventory(args["inventoryfile"]) + + # Open up the network configuration fil + netconf = config.Topology(args["networkfile"]) + + # Assign yourself an ip + #bring_up_admin_ip(netconf.networks["admin"].installer_ip) + + # Start dhcp server + dhcp.gen_dhcpd_file(args["dhcpdfile"], invconf.nodes, netconf.networks["admin"]) + if dhcp.start_server() != 0: + logger.error("Halting, cannot bring up dhcp server") + quit() + + + # Queue for holding test logs, makes program thread safe + logs_q = queue.Queue() + + # Start a new thread for the server that receives results + threading.Thread(target=receiver.start, \ + args=(invconf.nodecount(), args["server-port"], logs_q), \ + daemon=True).start() + + failed_nodes = ipmi.power_nodes(invconf.nodes, "on") + + # If the failed nodes list is not empty, then fail + if failed_nodes != []: + logger.error("Halting, {} were unable to be powered on".format(", ".join(failed_nodes))) + quit() + + admin_network = netconf.networks["admin"] + + ip_range = util.gen_ip_range(admin_network.cidr, [admin_network.installer_ip], admin_network.usable_ip_range.minimum, \ + admin_network.usable_ip_range.maximum) + + logger.info(ip_range) + + available_ips = server.ping_network(ip_range_list=ip_range, ipcnt=len(invconf.nodes), passes=20) + + logger.info(available_ips) + + # Start a thread to run tests on each different node, and setup + # their NICs + for ip in available_ips: + threading.Thread( \ + target=server.ssh_thread, \ + args=(str(ip), str(admin_network.installer_ip), str(args["port"]), 200), \ + daemon=True).start() + + while True: + logger.info("Awaiting test result...") + test_logger.info(logs_q.get()) + logger.info("Logging test result...") + if logs_q.empty(): + break + + logger.info("Finished test, check {} and {}".format(args["logfile"], args["test_log"])) + + +def configure_root_logger(loglevel, logfile): + # Add a file handler to the default logger + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + + # Configure the root logger + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.setLevel(loglevel) + stdout_handler.setFormatter(formatter) + rl_handler = logging.FileHandler(logfile) + rl_handler.setFormatter(formatter) + + root_logger = logging.getLogger() + root_logger.addHandler(rl_handler) + root_logger.addHandler(stdout_handler) + root_logger.setLevel(loglevel) + +def parse_args(): + """ + parse_args: parse the commandline arguments and configuration file into + a dictionary that can be easily passed and referenced by other functions + + input: None + + output: Dictionary of all commandline arguments and configuration file + settings + """ + logger = logging.getLogger(__name__) + + parser = argparse.ArgumentParser( \ + description='evaluates a system against the pharos specification') + + parser.add_argument('--version', + action="store_true", default=False, + help='display version then exit') + + parser.add_argument('-q', '--quiet', + action="store_true", default=False, + help='disable console output') + + parser.add_argument('-v', '--verbose', + action="store_true", default=False, + help='Enable debugging level output') + + parser.add_argument('-o', '--output', + type=str, default="yaml", + help='Define which machine readable format to output') + + # port that the client should connect to + parser.add_argument('-c', '--config', + type=str, default="/etc/pharosvalidator/config.yaml", + help='Configuration file to read') + + # port that the server should use + parser.add_argument('-p', '--port', + type=str, default=12121, + help='flag to determine if server or client behavior \ + should be used') + + args = vars(parser.parse_args()) + + # Read the configuration file first to get extra information + if os.path.isfile(args["config"]): + with open(args["config"], 'r') as fd: + conf = yaml.load(fd.read()) + else: + logger.error("Halting, unable to load configuration file") + quit(1) + + for field in args: + conf[field] = args[field] + args = conf + + return args + +if __name__ == "__main__": + main() diff --git a/validator/src/validation_tool/doc/config.yaml b/validator/src/validation_tool/doc/config.yaml new file mode 100644 index 0000000..f2146ff --- /dev/null +++ b/validator/src/validation_tool/doc/config.yaml @@ -0,0 +1,37 @@ +--- +################### +# Program arguments +################### +# +# Define path of output file +output: "yaml" # Values are ("yaml", ) + +################# +# Server settings +################# + +# Address that the client should attempt to connect to. Has no effect on server +server-addr: "0.0.0.0" +# Server port, shouldn't need to change +server-port: 12121 + +############################# +# Filepaths for program files +############################# + +# Define directory for the program to monitor files pushed back by clients +tmppath: "/tmp/pharosqa" +# dhcpd configuration output filepath +confpath: "/etc/pharosqa/" +# Examplepath +sharepath: "/usr/share/pharosqa/" +# Path to inventory file +inventoryfile: "/etc/pharosvalidator/inventory.yaml" +# Path to network file +networkfile: "/etc/pharosvalidator/network.yaml" +# Path to logfile +logfile: "/tmp/runtime_logs.txt" +# Path to test logs +test_log: "/tmp/results.yaml" +# Path to dhcpd configuration file +dhcpdfile: "/etc/dhcp/dhcpd.conf" diff --git a/validator/src/validation_tool/doc/example/config.yaml b/validator/src/validation_tool/doc/example/config.yaml new file mode 100644 index 0000000..f2146ff --- /dev/null +++ b/validator/src/validation_tool/doc/example/config.yaml @@ -0,0 +1,37 @@ +--- +################### +# Program arguments +################### +# +# Define path of output file +output: "yaml" # Values are ("yaml", ) + +################# +# Server settings +################# + +# Address that the client should attempt to connect to. Has no effect on server +server-addr: "0.0.0.0" +# Server port, shouldn't need to change +server-port: 12121 + +############################# +# Filepaths for program files +############################# + +# Define directory for the program to monitor files pushed back by clients +tmppath: "/tmp/pharosqa" +# dhcpd configuration output filepath +confpath: "/etc/pharosqa/" +# Examplepath +sharepath: "/usr/share/pharosqa/" +# Path to inventory file +inventoryfile: "/etc/pharosvalidator/inventory.yaml" +# Path to network file +networkfile: "/etc/pharosvalidator/network.yaml" +# Path to logfile +logfile: "/tmp/runtime_logs.txt" +# Path to test logs +test_log: "/tmp/results.yaml" +# Path to dhcpd configuration file +dhcpdfile: "/etc/dhcp/dhcpd.conf" diff --git a/validator/src/validation_tool/doc/example/inventory.yaml b/validator/src/validation_tool/doc/example/inventory.yaml new file mode 100644 index 0000000..2ba768a --- /dev/null +++ b/validator/src/validation_tool/doc/example/inventory.yaml @@ -0,0 +1,38 @@ +--- +nodes: + - name: node0 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:11:11" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.2 + user: root + pass: root + - name: node1 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:22:22" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.3 + user: root + pass: root + - name: node2 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:33:33" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.4 + user: root + pass: root diff --git a/validator/src/validation_tool/doc/example/network.yaml b/validator/src/validation_tool/doc/example/network.yaml new file mode 100644 index 0000000..91c1be9 --- /dev/null +++ b/validator/src/validation_tool/doc/example/network.yaml @@ -0,0 +1,207 @@ +--- +# This configuration file defines Network Environment for a +# Baremetal Deployment of OPNFV. It contains default values +# for 5 following networks: +# +# - admin +# - tenant* +# - external* +# - storage* +# - api* +# *) optional networks +# +# Optional networks will be consolidated with the admin network +# if not explicitly configured. +# +# See short description of the networks in the comments below. +# +# "admin" is the short name for Control Plane Network. +# During OPNFV deployment it is used for node provisioning which will require +# PXE booting as well as running a DHCP server on this network. Be sure to +# disable any other DHCP/TFTP server on this network. +# +# "tenant" is the network used for tenant traffic. +# +# "external" is the network which should have internet or external +# connectivity. External OpenStack networks will be configured to egress this +# network. There can be multiple external networks, but only one assigned as +# "public" which OpenStack public API's will register. +# +# "storage" is the network for storage I/O. +# +# "api" is an optional network for splitting out OpenStack service API +# communication. This should be used for IPv6 deployments. + +network-config-metadata: # Meta data for the network configuration + title: LF-POD-1 Network config # + version: 0.1 # + created: Mon Dec 28 2015 # + comment: None # + +# yamllint disable rule:line-length +networks: # Network configurations + admin: # Admin configuration (pxe and jumpstart), + enabled: true + vlan: native # VLAN tag to use for Overcloud hosts on this network + installer_vm: # Network settings for the Installer VM on admin network + nic_type: interface # Indicates if this VM will be bridged to an interface, or to a bond + members: + - em1 # Member Interface to bridge to for installer VM (use multiple values for bond) + vlan: 29 # VLAN tag to use for this network on Installer VM, native means none + ip: 10.10.25.100 # IP to assign to Installer VM on this network + usable_ip_range: 10.10.25.0,10.10.25.100 # Usable ip range, if empty entire range is usable, ex. 192.168.1.10,192.168.1.20 + gateway: 192.0.2.1 # Gateway (only needed when public_network is disabled) + cidr: 10.10.25.0/24 # Subnet in CIDR format 192.168.1.0/24 + dhcp_range: 10.10.25.50,10.10.25.99 # DHCP range for the admin network, if empty it will be automatically provisioned + dns-domain: opnfvlf.org # Admin network dns domain + dns-search: opnfvlf.org # Admin network dns-search domain + dns-upstream: # Admin network upstream dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp-upstream: # Admin upstream ntp servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: native # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: native + members: + - eth1 + - eth2 + + tenant: # Tenant network configuration + enabled: true + cidr: 11.0.0.0/24 # Subnet in CIDR format 192.168.1.0/24 + vlan: 10 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + overlay_id_range: 2,65535 # Tenant network Overlay segmentation ID range: VNI, VLAN-ID, etc. + + segmentation_type: vxlan # Tenant network segmentation type:vlan, vxlan, gre + + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 10 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 10 + members: + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + + external: # Can contain 1 or more external networks + - public: # "public" network will be the network the installer VM attaches to + enabled: true + vlan: native + mtu: 64000 # Public network MTU + installer_vm: # Network settings for the Installer VM on admin network (note only valid on 'public' external network) + nic_type: interface # Indicates if this VM will be bridged to an interface, or to a bond + members: + - em1 # Member Interface to bridge to for installer VM (use multiple values for bond) + ip: 192.168.37.12 # IP to assign to Installer VM on this network + cidr: 192.168.37.0/24 + gateway: 192.168.37.1 + floating_ip_range: 192.168.37.200,192.168.37.220 # Range to allocate to floating IPs for the public network with Neutron + usable_ip_range: 192.168.37.10,192.168.37.199 # Usable IP range on the public network, usually this is a shared subnet + dns_nameservers: # External dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp: # External upstream NTP servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + syslog: # External Syslog server + server: 10.128.1.24 # + transport: 'tcp' # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 10 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 10 + members: + - eth1 + external_overlay: # External network to be created in OpenStack by Services tenant + name: Public_internet + type: flat + gateway: 192.168.37.1 + - private_cloud: # another external network + enabled: false + vlan: 101 + mtu: 64000 + cidr: 192.168.38.0/24 + gateway: 192.168.38.1 + floating_ip_range: 192.168.38.200,192.168.38.220 # Range to allocate to floating IPs for the public network with Neutron + usable_ip_range: 192.168.38.10,192.168.38.199 # Usable IP range on the public network, usually this is a shared subnet + dns_nameservers: # External dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp: # External upstream NTP servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + syslog: # External Syslog server + server: 10.128.1.24 # + transport: 'tcp' # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 101 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 101 + members: + - eth1 + external_overlay: # External network to be created in OpenStack by Services tenant + name: private_cloud + type: vlan + segmentation_id: 101 + gateway: 192.168.38.1 + + storage: # Storage network configuration + enabled: true + cidr: 12.0.0.0/24 # Subnet in CIDR format + vlan: 12 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: bond # Physical interface type (interface or bond) + vlan: 12 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth3 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + - eth4 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: 12 + members: + - eth3 + - eth4 + + api: # API network configuration + enabled: false + cidr: fd00:fd00:fd00:4000::/64 # Subnet in CIDR format + vlan: 13 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: bond # Physical interface type (interface or bond) + vlan: 13 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth3 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + - eth4 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: 13 + members: + - eth3 + - eth4 +# yamllint enable rule:line-length diff --git a/validator/src/validation_tool/doc/inventory.yaml b/validator/src/validation_tool/doc/inventory.yaml new file mode 100644 index 0000000..2ba768a --- /dev/null +++ b/validator/src/validation_tool/doc/inventory.yaml @@ -0,0 +1,38 @@ +--- +nodes: + - name: node0 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:11:11" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.2 + user: root + pass: root + - name: node1 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:22:22" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.3 + user: root + pass: root + - name: node2 + tags: control # optional param, other valid value "compute" + arch: "x86_64" + mac_address: "de:ad:be:ef:33:33" # pxe boot interface mac + cpus: 2 # required only for virtual deployments + memory: 2048 # required only for virtual deployments + disk: 40 # required only for virtual deployments + power: + type: ipmi + address: 10.4.7.4 + user: root + pass: root diff --git a/validator/src/validation_tool/doc/network.yaml b/validator/src/validation_tool/doc/network.yaml new file mode 100644 index 0000000..705e848 --- /dev/null +++ b/validator/src/validation_tool/doc/network.yaml @@ -0,0 +1,221 @@ +--- +# This configuration file defines Network Environment for a +# Baremetal Deployment of OPNFV. It contains default values +# for 5 following networks: +# +# - admin +# - tenant* +# - external* +# - storage* +# - api* +# *) optional networks +# +# Optional networks will be consolidated with the admin network +# if not explicitly configured. +# +# See short description of the networks in the comments below. +# +# "admin" is the short name for Control Plane Network. +# During OPNFV deployment it is used for node provisioning which will require +# PXE booting as well as running a DHCP server on this network. Be sure to +# disable any other DHCP/TFTP server on this network. +# +# "tenant" is the network used for tenant traffic. +# +# "external" is the network which should have internet or external +# connectivity. External OpenStack networks will be configured to egress this +# network. There can be multiple external networks, but only one assigned as +# "public" which OpenStack public API's will register. +# +# "storage" is the network for storage I/O. +# +# "api" is an optional network for splitting out OpenStack service API +# communication. This should be used for IPv6 deployments. + +# yamllint disable rule:line-length +network-config-metadata: # Meta data for the network configuration + title: LF-POD-1 Network config # + version: 0.1 # + created: Mon Dec 28 2015 # + comment: None # + + +networks: # Network configurations + admin: # Admin configuration (pxe and jumpstart), + enabled: true + vlan: native # VLAN tag to use for Overcloud hosts on this network (Admin network is required to be native / untagged for PXE booting) + installer_vm: # Network settings for the Installer VM on admin network + nic_type: interface # Indicates if this VM will be bridged to an interface, or to a bond + members: + - em1 # Member Interface to bridge to for installer VM (use multiple values for bond) + vlan: native # VLAN tag to use for this network on Installer VM, native means none + ip: 192.0.2.1 # IP to assign to Installer VM on this network + usable_ip_range: 192.0.2.11,192.0.2.99 # Usable ip range, if empty entire range is usable, ex. 192.168.1.10,192.168.1.20 + gateway: 192.0.2.1 # Gateway (only needed when public_network is disabled) + cidr: 192.0.2.0/24 # Subnet in CIDR format 192.168.1.0/24 + dhcp_range: 192.0.2.2,192.0.2.10 # DHCP range for the admin network, if empty it will be automatically provisioned + dns-domain: opnfvlf.org # Admin network dns domain + dns-search: opnfvlf.org # Admin network dns-search domain + dns-upstream: # Admin network upstream dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp-upstream: # Admin upstream ntp servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: native # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: native + members: + - eth1 + - eth2 + + tenant: # Tenant network configuration + enabled: true + cidr: 11.0.0.0/24 # Subnet in CIDR format 192.168.1.0/24 + vlan: 10 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + overlay_id_range: 2,65535 # Tenant network Overlay segmentation ID range: VNI, VLAN-ID, etc. + + segmentation_type: vxlan # Tenant network segmentation type: vlan, vxlan, gre + + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 10 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 10 + members: + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + + external: # Can contain 1 or more external networks + - public: # "public" network will be the network the installer VM attaches to + enabled: true + vlan: native + mtu: 64000 # Public network MTU + installer_vm: # Network settings for the Installer VM on admin network (note only valid on 'public' external network) + nic_type: interface # Indicates if this VM will be bridged to an interface, or to a bond + members: + - em1 # Member Interface to bridge to for installer VM (use multiple values for bond) + ip: 192.168.37.12 # IP to assign to Installer VM on this network + cidr: 192.168.37.0/24 + gateway: 192.168.37.1 + floating_ip_range: 192.168.37.200,192.168.37.220 # Range to allocate to floating IPs for the public network with Neutron + usable_ip_range: 192.168.37.10,192.168.37.199 # Usable IP range on the public network, usually this is a shared subnet + dns_nameservers: # External dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp: # External upstream NTP servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + syslog: # External Syslog server + server: 10.128.1.24 # + transport: 'tcp' # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 10 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 10 + members: + - eth1 + external_overlay: # External network to be created in OpenStack by Services tenant + name: Public_internet + type: flat + gateway: 192.168.37.1 + - private_cloud: # another external network + enabled: false + vlan: 101 + mtu: 64000 + cidr: 192.168.38.0/24 + gateway: 192.168.38.1 + floating_ip_range: 192.168.38.200,192.168.38.220 # Range to allocate to floating IPs for the public network with Neutron + usable_ip_range: 192.168.38.10,192.168.38.199 # Usable IP range on the public network, usually this is a shared subnet + dns_nameservers: # External dns servers + - 8.8.8.8 # + - 8.8.4.4 # + ntp: # External upstream NTP servers + - 0.se.pool.ntp.org # + - 1.se.pool.ntp.org # + syslog: # External Syslog server + server: 10.128.1.24 # + transport: 'tcp' # + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: interface # Physical interface type (interface or bond) + vlan: 101 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth1 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: interface + vlan: 101 + members: + - eth1 + external_overlay: # External network to be created in OpenStack by Services tenant + name: private_cloud + type: vlan + segmentation_id: 101 + gateway: 192.168.38.1 + + storage: # Storage network configuration + enabled: true + cidr: 12.0.0.0/24 # Subnet in CIDR format + vlan: 12 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: bond # Physical interface type (interface or bond) + vlan: 12 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth3 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + - eth4 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: 12 + members: + - eth3 + - eth4 + + api: # API network configuration + enabled: false + cidr: fd00:fd00:fd00:4000::/64 # Subnet in CIDR format + vlan: 13 # VLAN tag to use for Overcloud hosts on this network + mtu: 64000 # Tenant network MTU + nic_mapping: # Mapping of network configuration for Overcloud Nodes + compute: # Mapping for compute profile (nodes that will be used as Compute nodes) + phys_type: bond # Physical interface type (interface or bond) + vlan: 13 # VLAN tag to use with this NIC + members: # Physical NIC members of this mapping (Single value allowed for interface phys_type) + - eth3 # Note, for Apex you may also use the logical nic name (found by nic order), such as "nic1" + - eth4 + controller: # Mapping for controller profile (nodes that will be used as Controller nodes) + phys_type: bond + vlan: 13 + members: + - eth3 + - eth4 + +# JOID specific settings +joid: + +# Compass specific settings +compass: + +# Apex specific settings +apex: + networks: + admin: + introspection_range: 192.0.2.100,192.0.2.120 # Range used for introspection phase (examining nodes) +# Fuel specific settings +fuel: diff --git a/validator/src/validation_tool/doc/pharos-validator.1 b/validator/src/validation_tool/doc/pharos-validator.1 new file mode 100644 index 0000000..c76176f --- /dev/null +++ b/validator/src/validation_tool/doc/pharos-validator.1 @@ -0,0 +1,54 @@ +.TH pharoscmp 1 "2016-06-01" "version 0.1" +.SH NAME +pharoscmp - Testing tool for Pharos specification + +.SH SYNOPSIS +.B pharoscmp [ options ] + +.SH DESCRIPTION +This tool uses the virtual Linux filesystems to determine a computer's hardware and evaluate it against the Pharos specification to see if said computer is sufficient for being a node as part of the pod. + +.SH OPTIONS +.BR \-h ", " \-\-help +Show a help message and exit +.PP +.BR \-\-version +Show program version +.PP +.BR \-c ", " \-\-color +Enable colored console output +.PP +.BR \-v ", " \-\-verbose +Enable more verbose console output +.PP +.BR \-q ", " \-\-quiet +Disable console output +.PP +.BR \-o ", " \-\-output +Define path to output file for yaml file of results +.PP +.BR \-f ", " \-\-force +Forcefully override file defined by --output +.PP +.BR \-P ", " \-\-path +Path to the directory where the test should poll for the file called "nodeinfo.yaml" to arrive in. This file is usually sent via ssh to wherever the test is running +.PP + +.SH BUGS +Please report bugs to https://tommy.iol.unh.edu/redmine/projects/pharoscmp + +.SH AUTHOR +AUTHOR INFO GOES HERE + +.SH 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 + +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/validator/src/validation_tool/requirements.txt b/validator/src/validation_tool/requirements.txt new file mode 100644 index 0000000..dd5a2e6 --- /dev/null +++ b/validator/src/validation_tool/requirements.txt @@ -0,0 +1,3 @@ +netifaces +watchdog +pytest diff --git a/validator/src/validation_tool/setup.py b/validator/src/validation_tool/setup.py new file mode 100755 index 0000000..6b00b38 --- /dev/null +++ b/validator/src/validation_tool/setup.py @@ -0,0 +1,31 @@ +#! /usr/bin/env python3 + +from distutils.core import setup + +import subprocess + +setup( + name = 'pharos-validator', + description = 'Testing tool for Pharos spec compliance', + author = 'Todd Gaunt', + url = '', + download_url = '', + author_email = 'singularik@iol.unh.edu', + version = '0.1', + license = 'TBD', + + packages = ['pharosvalidator', + 'pharosvalidator.test'], + + package_dir = {'pharosvalidator':'src', + 'pharosvalidator.test':'src/test'}, + + # Change these per distribution + data_files = [('share/man/man1/', ['doc/pharos-validator.1']), + ('share/licenses/pharos-validator/LICENSE', ['LICENSE']), + ('share/pharos-validator/', ['doc/config.yaml', 'doc/inventory.yaml', 'doc/network.yaml']), + ], + + scripts = ['bin/pharos-validator-node', + 'bin/pharos-validator-server'] + ) diff --git a/validator/src/validation_tool/src/__init__.py b/validator/src/validation_tool/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/validator/src/validation_tool/src/config.py b/validator/src/validation_tool/src/config.py new file mode 100644 index 0000000..443467e --- /dev/null +++ b/validator/src/validation_tool/src/config.py @@ -0,0 +1,176 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import logging +import sys +import os +import yaml +import struct +import socket + +from pharosvalidator import util +from collections import namedtuple + +class Topology(): + """ + Topology: Class to store any number of Network classes + and metadata about them + """ + def __init__(self, yaml_config): + # Dictionary of available networks + self.logger = logging.getLogger(__name__) + self.networks = {} + self.external_networks = [] + + # Fill the above dictionaries + self.parse_yaml(yaml_config) + + def parse_yaml(self, yaml_config): + """ + parse_yaml: parses the yaml configuration file this program uses + for all the network and node information + """ + config = safe_yaml_read(yaml_config) + for network in config["networks"]: + self.logger.info("Reading network section {}".format(network)) + if network == "admin": + self.networks[network] = Network(config["networks"][network]) + #TODO + elif network == "external": + for external_network in config["networks"][network]: + self.external_networks.append(Network(external_network)) + +class Network(): + """ + Network: Class to store all information on a given network + """ + def __init__(self, network): + try: + self.logger = logging.getLogger(__name__) + + # Some generic settings + self.enabled = network["enabled"] + self.vlan = network["vlan"] + + # VM settings + self.installer_nic_type = network["installer_vm"]["nic_type"] + self.installer_members = network["installer_vm"]["members"] + self.installer_ip = network["installer_vm"]["ip"] + + # Tuple containing the minimum and maximum + self.usable_ip_range = self.parse_ip_range(network["usable_ip_range"]) + self.gateway = network["gateway"] + self.cidr = network["cidr"] + self.dhcp_range = network["dhcp_range"] + self.dns_domain = network["dns-domain"] + self.dns_search = network["dns-search"] + + subnet, netmask = self.split_cidr(network["cidr"]) + self.subnet = subnet + self.netmask = netmask + + # List of all dns servers + self.dns_upstream = network["dns-upstream"] + + self.nic_mapping = {} + except KeyError as e: + self.logger.error("Field {} not available in network configuration file".format(e)) + + def split_cidr(self, cidr): + """ + split_cidr: Split up cidr notation subnets into a subnet string and a + netmask string + + input: cidr notation of a subnet + + output: Subnet string; Netmask string + """ + split = cidr.split('/') + host_bits = int(split[1]) + netmask = self.cidr_to_netmask(host_bits) + subnet = split[0] + + return subnet, netmask + + def parse_ip_range(self, ip_range_string): + """ + parse_ip_range: Create a named tuple object that contains the lowest + ip address and the highest ip address from a configuration file + + input: String formatted like so "min, max" where min/max are ip addresses + + output: Named tuple object containing a minimum and maximum field + """ + rp = ip_range_string.split(",") + ip_range = namedtuple("ip_range", ['minimum', 'maximum'])(minimum=min(rp), maximum=max(rp)) + return ip_range + + def cidr_to_netmask(self, cidr): + bits = 0xffffffff ^ (1 << 32 - cidr) - 1 + netmask = socket.inet_ntoa(struct.pack('>I', bits)) + self.logger.debug("Netmask generated from cidr '{}': '{}'".format(cidr, netmask)) + return netmask + +class Inventory(): + """ + Inventory: Class to hold configuration file data + """ + def __init__(self, yaml_config): + # Create the class logger + self.logger = logging.getLogger(__name__) + + self.nodes = [] + + # Fill the above list + self.parse_yaml(yaml_config) + + def parse_yaml(self, yaml_config): + config = safe_yaml_read(yaml_config) + nodes = [] + for node in config["nodes"]: + self.nodes.append(Node(node)) + + def nodecount(self): + return len(self.nodes) + +class Node(): + """ + Node: Class to hold + """ + def __init__(self, node): + self.logger = logging.getLogger(__name__) + try: + self.name = node["name"] + self.tags = node["tags"] + self.arch = node["arch"] + self.mac_address = node["mac_address"] # ipmi mac address + self.cpus = node["cpus"] + self.memory = node["memory"] + self.disk = node["disk"] + except KeyError as e: + self.logger.error("Field {} not available in inventory file".format(e)) + + # Power sub section + if node["power"]["type"] == "ipmi": + try: + self.ipmi_addr = node["power"]["address"] + self.ipmi_user = node["power"]["user"] + self.ipmi_pass = node["power"]["pass"] + except KeyError as e: + self.logger.error("Field {} not available in inventory file".format(e)) + else: + pass + +def safe_yaml_read(yamlfile): + logger = logging.getLogger(__name__) + if os.path.isfile(yamlfile) == False: + logger.critical("Could not open find {}".format(yamlfile)) + quit(1) + with open(yamlfile, 'r') as fd: + return yaml.load(fd.read()) diff --git a/validator/src/validation_tool/src/const.py b/validator/src/validation_tool/src/const.py new file mode 100644 index 0000000..a204a96 --- /dev/null +++ b/validator/src/validation_tool/src/const.py @@ -0,0 +1,48 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +## Various constant strings used throughout program +HARDWARE_TEST="pharos-validator-node" + +## Pharos hardware specification +# memory +MIN_MEMSIZE = 32000000 # In Kb + +# cpu +MIN_CPUFREQ = 1800.000 # In Mhz +MIN_CORECOUNT = 4 + +# storage +MIN_DISKCOUNT = 3 +MIN_SSDCOUNT = 1 +MIN_HDDSIZE = 1000 # In Gb +MIN_SSDSIZE = 100 # In Gb +# Smallest possible disk size +MIN_DISKSIZE = min(MIN_HDDSIZE, MIN_SSDSIZE) + +# Virtual deployments +# Requirements are per node +APEX_REQ = {"cores": 2, \ + "ram": 8000000, \ + "disk": 40} + +# Requirements are per node +COMPASS_REQ = {"cores": 4, \ + "ram": 4000000, \ + "disk": 100} + +# Requirements are per node +JOID_REQ = {"cores": 4, \ + "ram": 4000000, \ + "disk": 100} + +# Requirements are per node +FUEL_REQ = {"cores": 4, \ + "ram": 4000000, \ + "disk": 100} diff --git a/validator/src/validation_tool/src/dhcp.py b/validator/src/validation_tool/src/dhcp.py new file mode 100644 index 0000000..26c42f8 --- /dev/null +++ b/validator/src/validation_tool/src/dhcp.py @@ -0,0 +1,102 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import yaml +import netifaces +import subprocess +import copy +import re +import os +import logging + +from pharosvalidator.specification import * +from pharosvalidator import util + +init_cmd = ["systemctl", "start", "dhcpd.service"] + +def gen_dhcpd_file(dhcpdfile, nodes, network): + """Generates and associates incremental ip addresses to + MAC addresses according to restrictions placed by network + configuration file. Writes all of this out in dhcpd.conf format""" + logger = logging.getLogger(__name__) + logger.info("Generating dhcpfile...") + + header = "default-lease-time 86400;\n\ + max-lease-time 604800;\n\ + max-lease-time 604800;\n\ + \n\ + allow booting;\n\ + authoritative;\n\ + \n" + + # Skip this network if it is disabled + if network.enabled == False: + logger.info("Admin network is disabled, please change the configuration to \"enabled\" if you would like this test to run") + quit() + + # Not explicitly in the cofiguration file + broadcastaddr = "0.0.0.0" + next_server = "0.0.0.0" + + ip_range = util.gen_ip_range(network.cidr, [network.installer_ip], network.usable_ip_range.minimum, \ + network.usable_ip_range.maximum) + + tab = ' ' + subnetconf = "subnet {} netmask {} {{\n".format(network.subnet, network.netmask)\ + + tab+"range {} {};\n".format(network.usable_ip_range.minimum, network.usable_ip_range.maximum)\ + + tab+"option broadcast-address {};\n".format(broadcastaddr)\ + + tab+'filename "pxelinux.0";\n'\ + + tab+"next-server {};\n".format(next_server) + + # For now no static addresses are assigned + """ + static_addrs = [] + for node in nodes: + # Skip the node if it doesn't have a name or mac address specified + if not node.name or not node.mac_address: + continue + + if node.ipmi_addr in ip_range: + ip_range.remove(node.ipmi_addr) + + static_line = "host {node} {{ hardware ethernet {ipmi_mac}; fixed-address {ip_addr}; }}\n".format\ + (node=node.name, ipmi_mac=node.mac_address, ip_addr=ip_range[0]) + ip_range = ip_range[1::] # Remove the assigned ip address + static_addrs.append(static_line) + + # Now add all statically assigned ip addresses + for addr in static_addrs: + subnetconf += tab+addr + """ + + subnetconf += "}\n" # Just the closing bracket + + # The final text to be written out to a file + dhcpdtext = header + subnetconf + + with open(dhcpdfile, "w+") as fd: + logger.info("Writing out dhcpd file to {}".format(dhcpdfile)) + fd.write(dhcpdtext) + + return dhcpdtext + +def start_server(): + logger = logging.getLogger(__name__) + global init_cmd + cmd = init_cmd + with open(os.devnull, 'w') as fn: + status = subprocess.Popen(cmd, stdout=fn, stderr=fn).wait() + if int(status) != 0: + logger.error("Could not bring up dhcpd server") + else: + logger.info("Dhcp server brought up") + return status + +if __name__ == "__main__": + split("inventory.yaml", "eth0") diff --git a/validator/src/validation_tool/src/ipmi.py b/validator/src/validation_tool/src/ipmi.py new file mode 100644 index 0000000..44be207 --- /dev/null +++ b/validator/src/validation_tool/src/ipmi.py @@ -0,0 +1,63 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import os +import subprocess +import logging + +def power_nodes(nodes, action): + """ Attempts to power on all nodes specified in a list, then returns a list + of the names of all failures. The list will be empty if no failures.""" + failed_nodes = [] + logger = logging.getLogger(__name__) + if not nodes: + logger.info("No nodes, is empty list") + for node in nodes: + # -I flag must be 'lanplus', 'lan' by itself doesn't work with + # the most recent idrac/ipmi version + if action == "on": + pass + elif action == "off": + pass + else: + logger.error("Invalid ipmi command") + + cmd = ["ipmitool", \ + "-I", "lanplus", \ + "-H ", "'"+node.ipmi_addr+"'", \ + "-U ", "'"+node.ipmi_user+"'", \ + "-P ", "'"+node.ipmi_pass+"'", \ + "power", action] + + logger.debug("Running: \"{}\"".format(' '.join(cmd))) + try: + with open(os.devnull, 'w') as fn: + status = subprocess.check_call(" ".join(cmd), \ + stdout=fn, stderr=fn, shell=True) + except subprocess.CalledProcessError as e: + status = e.returncode + logger.error("{} could not be accessed at {} (exit code {})".format(\ + node.name, node.ipmi_addr, status)) + failed_nodes.append(node.name) + if int(status) == 0: + logger.info("{} successfully powered {}".format(node.name, action)) + + return failed_nodes + +def status(node, ipaddr, username, passwd): + # -I flag must be 'lanplus', 'lan' by itself doesn't work with + # the most recent idrac/ipmi version + chkcmd = ["ipmitool", \ + "-I", "lanplus", \ + "-H", ipaddr, \ + "-U", username, \ + "-P", passwd, \ + "chassis", "status"] + print(chkcmd) + subprocess.Popen(chkcmd) diff --git a/validator/src/validation_tool/src/jenkins.py b/validator/src/validation_tool/src/jenkins.py new file mode 100644 index 0000000..443a615 --- /dev/null +++ b/validator/src/validation_tool/src/jenkins.py @@ -0,0 +1,8 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## diff --git a/validator/src/validation_tool/src/node.py b/validator/src/validation_tool/src/node.py new file mode 100644 index 0000000..280abb7 --- /dev/null +++ b/validator/src/validation_tool/src/node.py @@ -0,0 +1,85 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import logging +import socket +import yaml +import os + +import pharosvalidator.test.probe as probe +import pharosvalidator.test.evaluate as evaluate +from pharosvalidator.util import send_msg + +def hardware_test(): + """ + hardware_test: Run hardware probing/testing functions + + input: None + + output: String in YAML format of the tests that were run + """ + logger = logging.getLogger(__name__) + logger.info("Beginning hardware test") + + # Run test scripts + results = [] + results.append(testinterpreter("CPU test", evaluate.cpu, probe.cpu())) + results.append(testinterpreter("Memory test", evaluate.memory, probe.memory())) + results.append(testinterpreter("Storage test", evaluate.storage, probe.storage())) + + # Start generating the yaml file + yamltext = "" + for result in results: + yamltext += yaml.dump(result, default_flow_style=False) + return yamltext + +def network_test(networkfile): + logger = logging.getLogger(__name__) + logger.info("Beginning network test") + logger.info("Ending network test") + pass + +def send_result(host, port, result): + """ + send_result: Send the final test result to the central test server + + input: Host address of target; Port of target; String to send to server + + output: None + """ + logger = logging.getLogger(__name__) + logger.info("Sending test result") + + # Format the results properly + linecount = 0 + for c in result: + if c == "\n": + linecount += 1 + + result = str(linecount) + "\n" + result + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((host, int(port))) + send_msg(sock, result) + +def testinterpreter(name, test, dataset): + """High level function for test functions within this module to print out + their results in an ordered function while also writing out logs, + expects a list of testresults objects""" + + # Start the yaml file contents + data = {name:[]} + + # test the dataset + results = test(dataset) + + for result in results: + data[name].append(result) + + return data diff --git a/validator/src/validation_tool/src/receiver.py b/validator/src/validation_tool/src/receiver.py new file mode 100644 index 0000000..07d968e --- /dev/null +++ b/validator/src/validation_tool/src/receiver.py @@ -0,0 +1,46 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import socket +import threading +import logging + +from pharosvalidator.util import read_msg + +def start(nodecount, port, q): + """Start a server to retrieve the files from the nodes. Server will run + indefinetely until the parent process ends""" + logging.basicConfig(level=0) + logger = logging.getLogger(__name__) + + address = "" # Empty means act as a server on all available interfaces + + logger.info("Bringing up receiver server...") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((address, port)) + sock.listen(nodecount) # Max connections is the amount of nodes + + while True: + # Receive a descriptor for the client socket, cl stands for client + (clsock, claddress) = sock.accept() + logger.info("Received client connection...") + client_thread = threading.Thread(target=_server_accept_thread, \ + args=(clsock, claddress, q), daemon=True) + # Start a new thread to read the new client socket connection + client_thread.start() + + socket.close() + logger.info("Bringing down receiver server...") + +def _server_accept_thread(clsock, claddress, q): + """Read from the socket into the queue, then close the connection""" + logger = logging.getLogger(__name__) + q.put(read_msg(clsock)) + logger.info("Retreived message from socket") + clsock.close() diff --git a/validator/src/validation_tool/src/server.py b/validator/src/validation_tool/src/server.py new file mode 100644 index 0000000..91c9a4f --- /dev/null +++ b/validator/src/validation_tool/src/server.py @@ -0,0 +1,111 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import logging +import os +import subprocess +import time + +# Constant definitions +from pharosvalidator.const import * + +def ssh_thread(remoteaddr, returnaddr, port, passes): + """ + ssh_thread: the main loop of a thread the server spawns to connect to a node + over ssh. + + input: remoteaddr, returnaddr, and port to forward to run_remote_test; + passes to specify how many attempts should be made + """ + for i in range(passes): + status = run_remote_test(remoteaddr, returnaddr, port) + time.sleep(1) + +def run_remote_test(remoteaddr, returnaddr, port): + """ + run_remote_tests: ssh to a give remote address, and run a test program + on the remote machine specifying the address and port of where the results + should be sent (usually back to the machine this program was run on) + + input: ip address of the ssh target; Adress of the test results target; + Port of the test results target + + output: 0 if the test ran over ssh perfectly, non-zero if the test faild + """ + #TODO add way to keep attempting to ssh until service is up and running aka ping part 2 + logger = logging.getLogger(__name__) + + cmd = ["ssh", "root@"+remoteaddr, HARDWARE_TEST, \ + "-p", port, "-H", returnaddr, "hardware"] + + logger.debug("Running: {}".format(" ".join(cmd))) + try: + with open(os.devnull, 'w') as fn: + status = subprocess.check_call(" ".join(cmd), stdout=fn, stderr=fn, shell=True) + except subprocess.CalledProcessError as e: + status = e.returncode + logger.error("ssh attempt to '{}' failed".format(remoteaddr)) + + return status + +def ping_network(ip_range_list, ipcnt, passes): + """ + ping_network: Ping a range of ips until the amount of successful pings + reaches a number n + + input: List of ip addresses to be pinged; Counter for threshold + of successful pings; Number of iterations to pass + + output: List of ip addresses that were found to be up + """ + logger = logging.getLogger("pharosvalidator") + assert isinstance(ip_range_list, list) + ips_found = 0 + alive_ips = [] + for t in range(passes): + for addr in list(ip_range_list): + cmd = [ \ + "ping", \ + "-c", "1", \ + "-w", "1", \ + addr] + logger.debug("Running: \"{}\"".format(' '.join(cmd))) + try: + with open(os.devnull, 'w') as fn: + status = subprocess.check_call(" ".join(cmd), \ + stdout=fn, stderr=fn, shell=True) + except subprocess.CalledProcessError as e: + status = e.returncode + logger.error("Ping at '{}' failed".format(addr)) + # If the ip address was pinged successfully, then remove it from future attempts + if status == 0: + ips_found += 1 + logger.info("{} is up, {} total nodes up".format(addr, ips_found)) + + # Remove the ip that was successfully pinged from being tested again + ip_range_list.remove(addr) + + # Add the successfully pinged node to a list of successful pings + alive_ips.append(addr) + + if ips_found >= ipcnt: + break + + if ips_found >= ipcnt: + break + + return alive_ips + +def bring_up_admin_ip(ipaddr): + """ + Assign the machine this test is running on an address according to the + configuration file + """ + cmd = [""] + subprocess.Popen(cmd) diff --git a/validator/src/validation_tool/src/test/__init__.py b/validator/src/validation_tool/src/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/validator/src/validation_tool/src/test/evaluate.py b/validator/src/validation_tool/src/test/evaluate.py new file mode 100644 index 0000000..81a837d --- /dev/null +++ b/validator/src/validation_tool/src/test/evaluate.py @@ -0,0 +1,159 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import logging + +from pharosvalidator.util import approxsize + +# Constant macros +from pharosvalidator.const import * + +def cpu(cpudata): + """Compares system cpu against the pharos specification""" + results = [] + + # Architecture evaluation, a value of 63 or greater indicates at least a 64-bit OS + if cpudata["bitsize"] >= 63: + val = True + else: + val = False + result = {"architecture": { + "pass": val, + "description": str(cpudata["architecture"])}} + results.append(result) + + # Core evaluation + if cpudata["cores"] < MIN_CORECOUNT: + val = False + else: + val = True + desc = "Have {0}, Need at least {1}".format(cpudata["cores"], MIN_CORECOUNT) + result = {"corecount": { + "pass": val, + "description": desc}} + results.append(result) + + # Speed evaluation + i = 0 + for cpufreq in cpudata["frequency"]: + # Cpufrequency was not read if this is the result + if cpufreq == -1: + desc = "(Cpu freuency could not be read)" + else: + if approxsize(cpufreq, MIN_CPUFREQ, 5) or cpufreq > MIN_CPUFREQ: + val = True + else: + val = False + desc = "Have {:.2f}Mhz, Need at least ~= {:.2f}Mhz".format( \ + cpufreq, MIN_CPUFREQ) + result = {"cpu"+str(i): { + "pass": val, + "description": desc}} + results.append(result) + i += 1 + + return results + +def memory(memdata): + """Compares system meminfo object against the pharos specification""" + logger = logging.getLogger(__name__) + + results = [] + + logger.debug("required memory: {}, detected memory: {}".format(\ + MIN_MEMSIZE, memdata["size"])) + # Capacity evaluation + if approxsize(memdata["size"], MIN_MEMSIZE, 5) or memdata["size"] > MIN_MEMSIZE: + val = True + else: + val = False + + desc = "Have {:.2f}G, Need at least ~= {:.2f}G".format( \ + memdata["size"], MIN_MEMSIZE/1000000) + + result = {"memory capacity": { + "pass": val, + "description": desc}} + results.append(result) + + return results + +def storage(diskdata): + """Compares system storage against the Pharos specification""" + def sizecmp(a, b, unit): + if approxsize(a, b, 10) or a > b: + val = True + else: + val = False + desc = "capacity is {:.2f}{}, Need at least ~= {:.2f}{}".format(a, \ + unit, b, unit) + return (val,desc) + + results = [] + # Disk size evaluation (also counts the disks) + diskcount = {"ssd":0, "non-ssd":0} + for disk in diskdata["names"]: + if diskdata["rotational"][disk]: + disktype = "non-ssd" + diskcount["non-ssd"] += 1 + else: + disktype = "ssd" + diskcount["ssd"] += 1 + val, desc = sizecmp(diskdata["sizes"][disk], MIN_SSDSIZE, 'G') + data = diskdata["sizes"][disk] + result = {disk: { + "pass": val, + "description": "Disk type: disktype; " + desc}} + results.append(result) + + # Disk number evaluation + if sum(diskcount.values()) >= 3 and diskcount["ssd"] >= 1: + val = True + else: + val = False + desc = "Have {0} drives, Need at least {1} drives and {3} ssds".format( \ + sum(diskcount.values()), MIN_DISKCOUNT, \ + diskcount["ssd"], MIN_SSDCOUNT) + + data = diskcount + result = {"diskcount": { + "pass": val, + "description": desc}} + results.append(result) + return results + +""" +def netinterfaces(netfaces): + results = [] + for netface in netfaces: + if netface.status <= 0: + val = False + state = "down" + else: + val = True + state = "up" + try: + MACaddr = netface.MAC[0]["addr"] + except IndexError: + MACaddr = "no MAC" + if len(netface.addrs) > 0: + addrs = "" + for addr in netface.addrs: + if len(addrs) > 0: + addrs += ", " + addrs += addr['addr'] + addrs = "addresses: " + addrs + else: + addrs = "no address" + desc = "({0} is {1} with {2})".format(netface.name, state, addrs) + data = MACaddr + results.append(gen_yamltext(netface.name, val, desc, data)) + return results + """ + diff --git a/validator/src/validation_tool/src/test/probe.py b/validator/src/validation_tool/src/test/probe.py new file mode 100644 index 0000000..daeccbc --- /dev/null +++ b/validator/src/validation_tool/src/test/probe.py @@ -0,0 +1,137 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import os +import re +import sys +import platform +import subprocess +import netifaces +import logging + +from pharosvalidator.util import cd # Contains the pharos specification values + +# Static vars +mempath="/proc/meminfo" +cpuinfopath="/proc/cpuinfo" +cpupath="/sys/devices/system/cpu/" +diskpath="/sys/block/" + +def readproc(path): + """Reads and parses /proc from [path] argument files + and returns a hashmap of values""" + logger = logging.getLogger(__name__) + # Fail if path does not exist + try: + hashmap = {} + with open(path) as fd: + logger.debug("Reading {}".format(path)) + for line in fd: + data = line.split(":") + if len(data) == 2: + # Strip trailing characters from hashmap names and entries + # for less junk + hashmap[data[0].strip()] = data[1].strip() + return hashmap + except IOError: + logger.error("Path to file does not exist: {}".format(path)) + quit(1) + +def cpu(): + logger = logging.getLogger(__name__) + cpudata = {} + cpuinfo = readproc(cpuinfopath) + cpudata["bitsize"] = sys.maxsize.bit_length() + cpudata["architecture"] = platform.architecture()[0] + cpudata["cores"] = int(cpuinfo["cpu cores"]) + cpudata["frequency"] = [] + for i in range(cpudata["cores"]): + freqpath = "{0}/cpu{1}/cpufreq/cpuinfo_max_freq".format(cpupath, \ + str(i)) + try: + with open(freqpath) as fd: + logger.debug("Opening {}".format(freqpath)) + cpufreq = (float(fd.read(-1)))/1000 + except IOError: + # Less accurate way of getting cpu information as + # this frequency may change during operation, + # if dynamic frequency scaling is enabled, + # however it is better than nothing. + logger.error("Path to file does not exist: {}".format(freqpath)) + logger.error("Reading cpu frequency from {} instead".format(freqpath)) + cpufreq = float(cpuinfo["cpu MHz"]) + + if cpufreq < 0: + cpudata["frequency"].append(0) + else: + cpudata["frequency"].append(cpufreq) + + return cpudata + +def memory(): + logger = logging.getLogger(__name__) + meminfo=readproc(mempath) + # Create the memory object to store memory information + memdata = {} + memdata["size"] = (int(meminfo["MemTotal"].split(' ')[0]))/1000000 + return memdata + +def storage(): + """Gather's disk information""" + logger = logging.getLogger(__name__) + diskdata = {"names":[],"rotational":{},"sizes":{}} + for disk in os.listdir(diskpath): + #sdX is the naming schema for IDE/SATA interfaces in Linux + if re.match(r"sd\w",disk): + logger.debug("Found disk {}".format(disk)) + diskdata["names"].append(disk) + sizepath = "{0}/{1}/size".format(diskpath, disk) + try: + with open(sizepath) as fd: + size = int(fd.read(-1)) + except IOError: + size = -1 + # If the read was successful + if size != -1: + # Converts the value to Gb + diskdata["sizes"][disk] = (size * 512)/1000000000 + + rotationalpath = "{0}/{1}/queue/rotational".format(diskpath, disk) + try: + with open(rotationalpath) as fd: + rotational = int(fd.read(-1)) + except IOError: + rotational = -1 + if rotational == 0: + diskdata["rotational"][disk] = False + else: + diskdata["rotational"][disk] = True + + return diskdata + +def netinterfaces(nodeinfo): + """Uses netifaces to probe the system for network interface information""" + netfaces = [] + for interface in netifaces.interfaces(): + netface = netdata() + netface.name = interface + tmp = netifaces.ifaddresses(interface) + # If the interface is up and has at least one ip address + if netifaces.AF_INET in tmp: + netface.status = 1 # 1 stands for "up" + netface.addrs = tmp[netifaces.AF_INET] + # If the interface is down + else: + netface.status = 0 # 0 stands for "down" + # The file /proc/net/arp may also be used to read MAC addresses + if netifaces.AF_LINK in tmp: + netface.MAC = tmp[netifaces.AF_LINK] + netfaces.append(netface) + + return netfaces diff --git a/validator/src/validation_tool/src/util.py b/validator/src/validation_tool/src/util.py new file mode 100644 index 0000000..67a75a5 --- /dev/null +++ b/validator/src/validation_tool/src/util.py @@ -0,0 +1,107 @@ +############################################################################## +# Copyright (c) 2015 Todd Gaunt and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import ipaddress +import logging +import os + +class cd: + """Context manager for changing the current working directory""" + def __init__(self, new_path): + self.new_path = os.path.expanduser(new_path) + + def __enter__(self): + self.saved_path = os.getcwd() + os.chdir(self.new_path) + + def __exit__(self, etype, value, traceback): + os.chdir(self.saved_path) + +def approxsize(x, y, deviation): + """Approximately compares 'x' to 'y' with in % of 'deviation'""" + logger = logging.getLogger(__name__) + + dev = (y * .01 * deviation) + + if x >= round(y - dev, 0) and x <= round(y + dev, 0): + logger.debug("{} is approximately {}".format(x, y)) + return True + else: + logger.debug("{} is not approximately {}".format(x, y)) + return False + +def read_line(sock): + """Reads from a socket until a \n character or 512 bytes have been read, + whichever comes first""" + c = "" + recvline = "" + reads = 0 + while (c != "\n" and reads < 512): + # Decode bytes to str, sockets output bytes which aren't pretty + c = sock.recv(1).decode("utf-8") + #print("char: '" + c + "'") # Debugging code + recvline += c + reads += 1 + return recvline + +def read_msg(sock): + """Reads a message prefixed with a number and a newline char, eg. "20\n" + then reads x lines, where x is equal to the number in the first line.""" + # Read the socket once initially for the line count + buf = read_line(sock) + buf = buf[:-1] # Cut off the '\n' character + length = int(buf) + + lines = [] + for i in range(length): + lines.append(read_line(sock)) + return "".join(lines) + +def send_msg(sock, msg): + """Sends a message to a socket""" + # Encode python str to bytes beforehand, sockets only deal in bytes + msg = bytes(msg, "utf-8") + totalsent = 0 + while totalsent < len(msg): + sent = sock.send(msg[totalsent:]) + if sent == 0: + return -1 + totalsent = totalsent + sent + return totalsent + +def get_addr(interface): + """Get the address of the machine that this program is running on""" + return netifaces.ifaddresses(interface)[netifaces.AF_INET][0]["addr"] + +def gen_ip_range(cidr, excluded, minimum, maximum ): + """Takes a network cidr number, and then a min max value, and creates a list + of ip addresses avalable on [a,b]. Also removes "excluded" addresses + from the range""" + logger = logging.getLogger(__name__) + # Generate a list of available ip addresses for the dhcp server + ip_range = list(map(lambda x: x.exploded, ipaddress.ip_network(cidr).hosts())) + + for addr in excluded: + # Remove the value from the list, if it isn't in the list then whatever + try: + ip_range.remove(addr) + except ValueError: + logger.debug("{} not in ip_range, cannot remove".format(addr)) + + # Remove all values before the minimum usable value + for i in range(len(ip_range)): + if ip_range[i] == minimum: + ip_range = ip_range[i::] + break + # Remove all values after the maximum usable value + for i in range(len(ip_range)): + if ip_range[i] == maximum: + ip_range = ip_range[0:i+1] + break + return ip_range diff --git a/validator/src/validation_tool/tests/test_node.py b/validator/src/validation_tool/tests/test_node.py new file mode 100644 index 0000000..e69de29 diff --git a/validator/src/validation_tool/tests/test_probe.py b/validator/src/validation_tool/tests/test_probe.py new file mode 100644 index 0000000..e69de29 diff --git a/validator/src/validation_tool/tests/test_server.py b/validator/src/validation_tool/tests/test_server.py new file mode 100644 index 0000000..35388b6 --- /dev/null +++ b/validator/src/validation_tool/tests/test_server.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +def test_ssh_thread(): + """Test to see if ssh connections are attempted the proper amount of times""" + from pharosvalidator.server import ssh_thread + ssh_thread("127.0.0.1", "0.0.0.0", "1", 10) + + + diff --git a/validator/src/validation_tool/tests/test_util.py b/validator/src/validation_tool/tests/test_util.py new file mode 100644 index 0000000..9ce939b --- /dev/null +++ b/validator/src/validation_tool/tests/test_util.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 + +import random + +def test_approxsize(): + from pharosvalidator.util import approxsize + assert approxsize(100, 95, 5) == True + assert approxsize(100, 105, 5) == True + + assert approxsize(100, 94, 5) == False + assert approxsize(100, 106, 5) == False + -- cgit 1.2.3-korg