Skip to content

Commit

Permalink
Merge pull request #12 from adambaumeister/api_integration
Browse files Browse the repository at this point in the history
Api integration
  • Loading branch information
adambaumeister authored Sep 20, 2019
2 parents bfe553c + 6bd6558 commit ddbe7b7
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 53 deletions.
34 changes: 30 additions & 4 deletions Remotes/gcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ 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
:param type: Device type (panorama|panos)
: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 = {
Expand All @@ -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
}
Expand All @@ -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
20 changes: 17 additions & 3 deletions Remotes/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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.")
Expand All @@ -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.")
Expand Down Expand Up @@ -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
"""
Expand All @@ -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)
Expand Down
23 changes: 20 additions & 3 deletions panos/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()):
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ pytest
google-cloud-firestore
coverage
coverage-badge
pytest
pytest
beautifultable
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -24,6 +24,7 @@
"jinja2",
"passlib",
"requests",
"beautifultable",
],
classifiers=[
'Development Status :: 5 - Production/Stable',
Expand Down
110 changes: 69 additions & 41 deletions skilletcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ")
Expand All @@ -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,
Expand All @@ -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()
Expand Down Expand Up @@ -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')
Expand All @@ -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:
Expand Down

0 comments on commit ddbe7b7

Please sign in to comment.