diff --git a/Remotes/gcloud.py b/Remotes/gcloud.py index 683d5f3..c5e3ff2 100644 --- a/Remotes/gcloud.py +++ b/Remotes/gcloud.py @@ -5,12 +5,19 @@ class Gcloud(): """ Remote skillet retrieval from Gcloud based API - This class is for client-side utilties. + Usage:: + # Query for a specific snippet + from Remotes import Gcloud + gc = Gcloud(https://api-dot-skilletcloud-prod.appspot.com) + gc.Query('iron-skillet', 'panos', 'snippets', ['tag'], '9.0', {}) + + # List all snippets + json_data = gc.List('iron-skillet') """ def __init__(self, url): self.url = url - def Query(self, skillet_name, type, stack, snippet_names, context): + def Query(self, skillet_name, type, stack, snippet_names, major_version, context): """ Query the skillet API. :param skillet_name: Name of skillet, such as iron-skillet, to query @@ -18,6 +25,7 @@ def Query(self, skillet_name, type, stack, snippet_names, context): :param stack: Stack to retrieve snippets from (snippets) :param snippet_names: List of snippets to retrieve :param context: Template variables + :param panos_version: :return: List of Snippet instances. """ QUERY = { @@ -26,6 +34,7 @@ def Query(self, skillet_name, type, stack, snippet_names, context): "name": snippet_names, "type": type, "stack": stack, + "panos_version": major_version, }, "template_variables": context } @@ -43,7 +52,24 @@ def Query(self, skillet_name, type, stack, snippet_names, context): return snippets - def List(self, skillet_name, t, stack_name): - res = requests.get(self.url + "/snippet?skillet={}&stack={}&type={}".format(skillet_name, stack_name, t)) + def List(self, skillet_name, **kwargs): + """ + List all snippets in a Skillet as JSON. + + :param skillet_name: Skillet to query + :param kwargs: Key/value filters to append to filter. + :return: Json representation of snippets + """ + params = [] + for k,v in kwargs.items(): + params.append('{}={}'.format(k,v)) + + qs = "&".join(params) + + if len(params) > 0: + res = requests.get(self.url + "/snippet?skillet={}&{}".format(skillet_name, qs)) + else: + res = requests.get(self.url + "/snippet?skillet={}&{}".format(skillet_name, qs)) + j = res.json() return j \ No newline at end of file diff --git a/Remotes/github.py b/Remotes/github.py index 15b5126..3f2fa1d 100644 --- a/Remotes/github.py +++ b/Remotes/github.py @@ -16,6 +16,11 @@ class Github: """ Github remote Github provides a wrapper to Git instances and provides indexing/search methods. + + Usage:: + from Remotes import github + g = Github() + repos = g.index() """ def __init__(self, topic="skillets", user="PaloAltoNetworks"): self.url = "https://api.github.com" @@ -24,6 +29,10 @@ def __init__(self, topic="skillets", user="PaloAltoNetworks"): self.search_endpoint = "/search/repositories" def index(self): + """ + Retrieves the list of repositories as a list of Git instances. + :return: [ Github.Git ] + """ r = requests.get(self.url + self.search_endpoint, params="q=topic:{}+user:{}".format(self.topic, self.user)) j = r.json() self.check_resp(j) @@ -51,7 +60,8 @@ def __init__(self, repo_url, store=os.getcwd(), github_info=None): Initilize a new Git repo object :param repo_url: URL path to repository. :param store: Directory to store repository in. Defaults to the current directory. - :param github_info: (dict): All information as sourced from Github + :param github_info: (dict): If this object is initialized by the Github class, all the repo attributes from + Github """ if not check_git_exists(): print("A git client is required to use this repository.") @@ -70,7 +80,7 @@ def clone(self, name, ow=False, update=False): Clone a remote directory into the store. :param name: Name of repository :param ow: OverWrite, bool, if True will remove any existing directory in the location. - :return: + :return: (string): Path to cloned repository """ if not name: raise ValueError("Missing or bad name passed to Clone command.") @@ -110,6 +120,7 @@ def clone(self, name, ow=False, update=False): def branch(self, branch_name): """ Checkout the specified branch. + :param branch_name: Branch to checkout. :return: None """ @@ -135,7 +146,10 @@ def list_branches(self): def build(self): """ Build the Skillet object using the git repository. - :return: + + Must be called after clone. + + :return: SkilletCollection instance """ if not self.Repo: self.clone(self.name) diff --git a/panos/device.py b/panos/device.py index ffe3265..ed8861d 100644 --- a/panos/device.py +++ b/panos/device.py @@ -5,7 +5,9 @@ import re class Panos: """ - PANOS Device. Could be a firewall or PANORAMA. + PANOS Device class. + + Represents either a firewall or panorama and provides a minimal interface to run commands. """ def __init__(self, addr, apikey=None, user="admin", pw=None, connect=True, debug=False, verify=False): """ @@ -64,7 +66,7 @@ def connect(self): def send(self, params): """ Send a request to this PANOS device - :param params: dict: GET parameters for query ({ "type": "op" }) + :param params: dict: POST parameters for query ({ "type": "op" }) :return: GET Response type """ url = self.url @@ -96,13 +98,28 @@ def get_type(self): print("Error on login received from PANOS: {}".format(r.text)) exit(1) + self.log(r.content) root = ElementTree.fromstring(r.content) + + # Get the device type elem = root.findall("./result/system/model") t = elem[0].text type_result = self.get_type_from_info(t) - self.log("Show sys model:{} Inferred type: {}".format(t, type_result)) + + # Get the device version + elem = root.findall("./result/system/sw-version") + self.sw_version = elem[0].text + self.major_sw_version = ".".join(self.sw_version.split(".")[0:2]) + self.log("Device details: {}:{} {} {}".format(type_result, t, self.sw_version, self.major_sw_version)) + return type_result + def get_version(self): + if not self.major_sw_version: + self.get_type() + + return self.major_sw_version + def get_type_from_info(self, t): for regex, result in self.type_switch.items(): if re.search(regex, t.lower()): diff --git a/requirements.txt b/requirements.txt index 6068451..7c7c6ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ pytest google-cloud-firestore coverage coverage-badge -pytest \ No newline at end of file +pytest +beautifultable \ No newline at end of file diff --git a/setup.py b/setup.py index 46ba7b6..0a32d40 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='skilletcli', - version='1.9.9', + version='2.0.0', packages=['Remotes', 'panos'], scripts=['skilletcli.py'], url='https://github.com/adambaumeister/skilletcli', @@ -24,6 +24,7 @@ "jinja2", "passlib", "requests", + "beautifultable", ], classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/skilletcli.py b/skilletcli.py index ebab101..2ac838a 100755 --- a/skilletcli.py +++ b/skilletcli.py @@ -14,29 +14,22 @@ from colorama import Fore, Back, Style import getpass import argparse -from Remotes import Git, Gcloud +from Remotes import Git, Gcloud, Github import json +from beautifultable import BeautifulTable """ -Create-and-push -Generates PANOS configuration from the XML snippets and adds to the PANOS device. +skilletcli -This way you can pick and choose the aspects of iron-skillet you want without removing your entire configuration. +This utility is designed as a CLI interface to PANOS configuration snippets. -Usage: - python create_and_push.py - With no arguments, lists all available snippets and their destination xpaths - - python create_and_push.py snippetname1 snippetname2 - Push the listed snippetnames +It includes several packges for retrieving snippets from supported backend storage types. """ -# Index of git based skillet repositories -GIT_SKILLET_INDEX = { - "iron-skillet": "https://github.com/PaloAltoNetworks/iron-skillet.git" -} # SKCLI credentials cache CREDS_FILENAME = ".skcli.json" KEY_DB = KeyDB(CREDS_FILENAME) +# API +DEFAULT_API_URL = "https://api-dot-skilletcloud-prod.appspot.com" def create_context(config_var_file): # read the metafile to get variables and values @@ -160,7 +153,25 @@ def push_from_gcloud(args): Pull snippets from Gcloud instead of Git repos. :param args: parsed args from argparse """ - print("{}Note: retrieval of snippets from webapi is currently experimental. Use with caution!{}".format(Fore.RED, Style.RESET_ALL)) + if args.repopath: + api_url = args.repopath + else: + api_url = DEFAULT_API_URL + + gc = Gcloud(api_url) + + if len(args.snippetnames) == 0: + print("{}New: browse the available objects via SkilletCloud: https://skilletcloud-prod.appspot.com/skillets/{}{}".format( + Fore.GREEN, args.repository, Style.RESET_ALL)) + snippets = gc.List(args.repository) + names = set() + for s in snippets: + names.add(s['name']) + + for n in names: + print(n) + + sys.exit(0) # Address must be passed, then lookup keystore if it exists. addr = env_or_prompt("address", args, prompt_long="address or address:port of PANOS Device to configure: ") @@ -174,16 +185,10 @@ def push_from_gcloud(args): fw = Panos(addr, apikey=apikey, debug=args.debug, verify=args.validate) t = fw.get_type() - gc = Gcloud(args.repopath) - context = create_context(args.config) - snippets = gc.Query(args.repository, t, args.snippetstack, args.snippetnames, context) + v = fw.get_version() - if len(args.snippetnames) == 0: - print("printing available {} snippets for type {} in stack {}".format(args.repository, t, args.snippetstack)) - snippet_names = gc.List(args.repository, t, args.snippetstack) - for sn in snippet_names: - print(sn) - sys.exit(0) + context = create_context(args.config) + snippets = gc.Query(args.repository, t, args.snippetstack, args.snippetnames, v, context) if len(snippets) == 0: print("{}Snippets {} not found for device type {}.{}".format(Fore.RED, ",".join(args.snippetnames), t, @@ -199,22 +204,47 @@ def push_skillets(args): :param args: parsed args from argparse """ if args.repotype == "git": - if args.repository not in GIT_SKILLET_INDEX: - if not args.repopath: - print("Non-registered skillet. --repopath [git url] is required.") - exit(1) - - repo_url = args.repopath + github = Github() + repo_list = github.index() + repo_url ='unset' + repo_table = BeautifulTable() + repo_table.set_style(BeautifulTable.STYLE_NONE) + repo_table.column_headers = ['Repository Name', 'Description'] + repo_table.column_alignments['Repository Name'] = BeautifulTable.ALIGN_LEFT + repo_table.column_alignments['Description'] = BeautifulTable.ALIGN_LEFT + repo_table.left_padding_widths['Description'] = 1 + repo_table.header_separator_char = '-' + if args.repository is None: + print('Available Repositories are:') + for repo in repo_list: + repo_table.append_row([repo.github_info['name'],repo.github_info['description']]) + print(repo_table) + exit() else: - repo_url = GIT_SKILLET_INDEX[args.repository] - + for repo in repo_list: + if repo.github_info['name'] == args.repository: + repo_url = repo.github_info['clone_url'] + break + if repo_url is 'unset': + print('Invalid Repository was specified. Available Repositories are:') + for repo in repo_list: + repo_table.append_row([repo.github_info['name'],repo.github_info['description']]) + print(repo_table) + exit() repo_name = args.repository g = Git(repo_url) g.clone(repo_name, ow=args.refresh, update=args.update) - if args.branch: - if args.branch == "list": - print("\n".join(g.list_branches())) - exit() + if args.branch is None: + print("Branches available for "+args.repository+" are :") + print("\n".join(g.list_branches())) + exit() + elif args.branch == "default": + print("Using default branch for repository.") + elif args.branch not in g.list_branches(): + print("Invalid Branch was choosen. Please select from below list:") + print("\n".join(g.list_branches())) + exit() + else: g.branch(args.branch) sc = g.build() @@ -271,10 +301,9 @@ def main(): script_options = parser.add_argument_group("Script options") kdb_options = parser.add_argument_group("Keystore options") - repo_arg_group.add_argument('--repository', default="iron-skillet", metavar="repo_name", help="Name of skillet to use" - .format(", ".join(GIT_SKILLET_INDEX.keys()))) + repo_arg_group.add_argument('--repository', default="iron-skillet", help="Name of skillet to use. Use without a value to see list of all available repositories.", nargs='?') repo_arg_group.add_argument('--repotype', default="git", help="Type of skillet repo. Available options are [git, api, local]") - repo_arg_group.add_argument("--branch", help="Git repo branch to use. Use 'list' to view available branches.") + repo_arg_group.add_argument("--branch", default="default", help="Git repo branch to use. Use without a value to view all available branches.",nargs='?') repo_arg_group.add_argument('--repopath', help="Path to repository if using local repo type") repo_arg_group.add_argument("--refresh", help="Refresh the cloned repository directory.", action='store_true') repo_arg_group.add_argument("--update", help="Update the cloned repository", action='store_true') @@ -295,8 +324,7 @@ def main(): args = parser.parse_args() if not args.validate: - print("""{}Warning: SSL validation is currently disabled. Use --validate to enable it.{} - """.format(Fore.YELLOW, Style.RESET_ALL)) + print("""{}Warning: SSL validation of PANOS device is currently disabled. Use --validate to enable it.{}""".format(Fore.YELLOW, Style.RESET_ALL)) requests.packages.urllib3.disable_warnings() if args.enable_keystore: