diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..7b96286
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+# Check http://editorconfig.org for more information
+# This is the main config file for this project:
+root = true
+
+[*]
+charset = utf-8
+trim_trailing_whitespace = true
+indent_style = space
+insert_final_newline = true
+
+[*.py]
+indent_size = 4
+
+[*.{bes,bes.mustache}]
+# bes files are XML, but the `actionscript` tag text must use crlf
+end_of_line = crlf
+indent_style = tab
+indent_size = 3
+
+[*.{bat,cmd}]
+end_of_line = crlf
diff --git a/.flake8 b/.flake8
index d1de6cc..79e952b 100644
--- a/.flake8
+++ b/.flake8
@@ -18,3 +18,4 @@ ignore = E127, E128, E203, E265, E266, E402, E501, E722, P207, P208, W503
exclude =
.git
__pycache__
+ examples
diff --git a/.gitconfig b/.gitconfig
new file mode 100644
index 0000000..17948d3
--- /dev/null
+++ b/.gitconfig
@@ -0,0 +1,6 @@
+[core]
+ hideDotFiles = true
+[rebase]
+ autoStash = true
+[pull]
+ rebase = true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..24d901f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+# Set update schedule for GitHub Actions
+version: 2
+
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+updates:
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ # Add assignees
+ assignees:
+ - "jgstew"
diff --git a/.github/workflows/black.yaml b/.github/workflows/black.yaml
index b8b38b6..249cf58 100644
--- a/.github/workflows/black.yaml
+++ b/.github/workflows/black.yaml
@@ -18,7 +18,7 @@ jobs:
name: runner / black formatter
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Run black formatter checks
# https://github.com/rickstaa/action-black
uses: rickstaa/action-black@v1
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f91c030..d4c32ae 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/flake8.yaml b/.github/workflows/flake8.yaml
index 8c12047..81838a3 100644
--- a/.github/workflows/flake8.yaml
+++ b/.github/workflows/flake8.yaml
@@ -20,8 +20,8 @@ jobs:
name: Python Lint Flake8
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.8"
- name: Install flake8
diff --git a/.github/workflows/grammar-check.yaml b/.github/workflows/grammar-check.yaml
new file mode 100644
index 0000000..f966c43
--- /dev/null
+++ b/.github/workflows/grammar-check.yaml
@@ -0,0 +1,20 @@
+---
+name: grammar-check
+
+on: workflow_dispatch
+
+jobs:
+ grammar-check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ target: x86_64-unknown-linux-gnu
+
+ - name: install harper grammar checker
+ run: cargo install --locked --git https://github.com/Automattic/harper.git --branch master --tag v0.23.0 harper-cli
+
+ - name: run harper grammar checker
+ run: harper-cli lint src/besapi/besapi.py
diff --git a/.github/workflows/isort.yaml b/.github/workflows/isort.yaml
index 3fb5ea7..999bae1 100644
--- a/.github/workflows/isort.yaml
+++ b/.github/workflows/isort.yaml
@@ -20,8 +20,8 @@ jobs:
name: runner / isort
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
with:
python-version: "3.8"
- name: Install isort
diff --git a/.github/workflows/misspell.yaml b/.github/workflows/misspell.yaml
index c116120..5711ee4 100644
--- a/.github/workflows/misspell.yaml
+++ b/.github/workflows/misspell.yaml
@@ -9,11 +9,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code.
- uses: actions/checkout@v1
+ uses: actions/checkout@v4
- name: misspell
if: ${{ !env.ACT }}
uses: reviewdog/action-misspell@v1
with:
github_token: ${{ secrets.github_token }}
locale: "US"
- reporter: github-check # Change reporter.
+ reporter: github-check # Change reporter.
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
new file mode 100644
index 0000000..69c5117
--- /dev/null
+++ b/.github/workflows/pre-commit.yaml
@@ -0,0 +1,17 @@
+---
+name: pre-commit
+
+on: pull_request
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ # requites to grab the history of the PR
+ fetch-depth: 0
+ - uses: actions/setup-python@v5
+ - uses: pre-commit/action@v3.0.1
+ with:
+ extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }}
diff --git a/.github/workflows/tag_and_release.yaml b/.github/workflows/tag_and_release.yaml
index fb55cb6..311013d 100644
--- a/.github/workflows/tag_and_release.yaml
+++ b/.github/workflows/tag_and_release.yaml
@@ -7,7 +7,8 @@ on:
- master
paths:
- "src/besapi/__init__.py"
- - ".github/workflows/tag_and_release.yaml"
+ - "src/besapi/besapi.py"
+ # - ".github/workflows/tag_and_release.yaml"
jobs:
release_new_tag:
@@ -15,19 +16,24 @@ jobs:
name: Tag and Release
runs-on: ubuntu-latest
steps:
- - name: "Checkout source code"
- uses: "actions/checkout@v1"
+ - name: Checkout source code
+ uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: 3.8
+
+ - name: Install requirements
+ run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
- name: Read VERSION file
id: getversion
run: echo "::set-output name=version::$(python ./setup.py --version)"
+
# only make release if there is NOT a git tag for this version
- name: "Check: package version has corresponding git tag"
# this will prevent this from doing anything when run through ACT
- if: ${{ !env.ACT }}
+ if: ${{ !env.ACT }} && contains(steps.getversion.outputs.version, '.')
id: tagged
shell: bash
run: git show-ref --tags --verify --quiet -- "refs/tags/v${{ steps.getversion.outputs.version }}" && echo "::set-output name=tagged::0" || echo "::set-output name=tagged::1"
@@ -35,15 +41,13 @@ jobs:
# what if no other tests?
- name: Wait for tests to succeed
if: steps.tagged.outputs.tagged == 1
- uses: lewagon/wait-on-check-action@v0.2
+ uses: lewagon/wait-on-check-action@v1.3.1
with:
ref: master
running-workflow-name: "Tag and Release"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 30
- - name: Install requirements
- if: steps.tagged.outputs.tagged == 1
- run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
- name: Install build tools
if: steps.tagged.outputs.tagged == 1
run: pip install setuptools wheel build
@@ -66,6 +70,6 @@ jobs:
${{ steps.getwheelfile.outputs.wheelfile }}
- name: Publish distribution to PyPI
if: steps.tagged.outputs.tagged == 1
- uses: pypa/gh-action-pypi-publish@master
+ uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/test_build.yaml b/.github/workflows/test_build.yaml
index 2d973a9..41e5916 100644
--- a/.github/workflows/test_build.yaml
+++ b/.github/workflows/test_build.yaml
@@ -4,7 +4,8 @@ name: test_build
on:
push:
paths:
- - "**.py"
+ - "src/**.py"
+ - "tests/**.py"
- "setup.cfg"
- "MANIFEST.in"
- "pyproject.toml"
@@ -13,7 +14,8 @@ on:
- ".github/workflows/tag_and_release.yaml"
pull_request:
paths:
- - "**.py"
+ - "src/**.py"
+ - "tests/**.py"
- "setup.cfg"
- "MANIFEST.in"
- "pyproject.toml"
@@ -26,42 +28,73 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, macos-latest]
+ os: [ubuntu-latest, windows-latest, macos-13]
# https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
- python-version: ["3.6", "3"]
+ python-version: ["3.9", "3"]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
+
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
+
- name: Install build tools
run: pip install setuptools wheel build pyinstaller
+
- name: Install requirements
+ shell: bash
run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+
+ - name: Read VERSION file
+ id: getversion
+ shell: bash
+ run: echo "$(python ./setup.py --version)"
+
- name: Run Tests - Source
run: python tests/tests.py
+
+ - name: Test invoke directly src/bescli/bescli.py
+ run: python src/bescli/bescli.py ls logout clear error_count version exit
+
+ - name: Test invoke directly src/besapi/besapi.py
+ run: python src/besapi/besapi.py ls logout clear error_count version exit
+
+ - name: Test invoke directly -m besapi
+ run: cd src && python -m besapi ls logout clear error_count version exit
+
+ - name: Test invoke directly -m bescli
+ run: cd src && python -m bescli ls logout clear error_count version exit
+
- name: Run build
run: python3 -m build
+
- name: Get Wheel File Path
id: getwheelfile
shell: bash
run: echo "::set-output name=wheelfile::$(find "dist" -type f -name "*.whl")"
+
- name: Test pip install of wheel
shell: bash
run: pip install $(find "dist" -type f -name "*.whl")
+
- name: Test python import besapi
shell: bash
- run: python -c "import besapi"
+ run: python -c "import besapi;print(besapi.besapi.__version__)"
+
- name: Test python import bescli
shell: bash
- run: python -c "import bescli"
+ run: python -c "import bescli;bescli.bescli.BESCLInterface().do_version()"
+
- name: Test python bescli
shell: bash
run: python -m bescli ls logout clear error_count version exit
+
- name: Run Tests - Pip
run: python tests/tests.py --test_pip
- - name: Test pyinstaller
+
+ - name: Test pyinstaller build
run: pyinstaller --clean --collect-all besapi --onefile ./src/bescli/bescli.py
+
- name: Test bescli binary
run: ./dist/bescli ls logout clear error_count version exit
diff --git a/.github/workflows/yamllint.yaml b/.github/workflows/yamllint.yaml
index 28557d0..720e7e9 100644
--- a/.github/workflows/yamllint.yaml
+++ b/.github/workflows/yamllint.yaml
@@ -15,10 +15,10 @@ jobs:
yamllint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: 3.8
diff --git a/.gitignore b/.gitignore
index 71bc149..467a361 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
besapi.conf
+tmp/
+
.DS_Store
# Byte-compiled / optimized / DLL files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 272f339..5097f6c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@
# https://github.com/pre-commit/pre-commit-hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.1.0
+ rev: v4.6.0
hooks:
- id: check-yaml
- id: check-json
@@ -27,7 +27,7 @@ repos:
# - id: no-commit-to-branch
# args: [--branch, main]
- repo: https://github.com/adrienverge/yamllint.git
- rev: v1.26.3
+ rev: v1.35.1
hooks:
- id: yamllint
args: [-c=.yamllint.yaml]
@@ -35,11 +35,21 @@ repos:
rev: v5.10.1
hooks:
- id: isort
- - repo: https://gitlab.com/pycqa/flake8
- rev: 3.9.2
- hooks:
- - id: flake8
- repo: https://github.com/psf/black
- rev: 22.1.0
+ rev: 25.1.0
hooks:
- id: black
+ - repo: https://github.com/python-jsonschema/check-jsonschema
+ rev: 0.31.2
+ hooks:
+ - id: check-github-workflows
+ args: ["--verbose"]
+ - id: check-dependabot
+ - repo: meta
+ hooks:
+ - id: check-useless-excludes
+ - id: check-hooks-apply
+ - repo: https://github.com/crate-ci/typos
+ rev: v1.30.0
+ hooks:
+ - id: typos
diff --git a/.pylintrc b/.pylintrc
index d2ec702..78baa69 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,5 +1,5 @@
[MESSAGES CONTROL]
-disable = C0330, C0326, C0103, c-extension-no-member, cyclic-import, no-self-use, unused-argument
+disable = C0103, c-extension-no-member, cyclic-import, no-self-use, unused-argument
[format]
max-line-length = 88
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..e4611ec
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "EditorConfig.EditorConfig",
+ "github.vscode-github-actions",
+ "ms-python.black-formatter",
+ "ms-python.python"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 6290b00..1c52634 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,10 +3,13 @@
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingImports": "information"
},
- "python.formatting.provider": "black",
+ "python.formatting.provider": "none",
"python.autoComplete.addBrackets": true,
"editor.formatOnSave": true,
"git.autofetch": true,
"python.analysis.completeFunctionParens": true,
- "python.linting.enabled": true
+ "python.linting.enabled": true,
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.black-formatter"
+ }
}
diff --git a/README.md b/README.md
index 171aabd..9050a08 100644
--- a/README.md
+++ b/README.md
@@ -91,27 +91,27 @@ OR
>>> import bescli
>>> bescli.main()
-BES> login
+BigFix> login
User [mah60]: mah60
Root Server (ex. https://server.institution.edu:52311): https://my.company.org:52311
Password:
Login Successful!
-BES> get help
+BigFix> get help
...
-BES> get sites
+BigFix> get sites
...
-BES> get sites.OperatorSite.Name
+BigFix> get sites.OperatorSite.Name
mah60
-BES> get help/fixlets
+BigFix> get help/fixlets
GET:
/api/fixlets/{site}
POST:
/api/fixlets/{site}
-BES> get fixlets/operator/mah60
+BigFix> get fixlets/operator/mah60
...
```
-# REST API Help
+# BigFix REST API Documentation
- https://developer.bigfix.com/rest-api/
- http://bigfix.me/restapi
@@ -124,6 +124,14 @@ BES> get fixlets/operator/mah60
- requests
- cmd2
+# Examples using BESAPI
+
+- https://github.com/jgstew/besapi/tree/master/examples
+- https://github.com/jgstew/generate_bes_from_template/blob/master/examples/generate_uninstallers.py
+- https://github.com/jgstew/jgstew-recipes/blob/main/SharedProcessors/BESImport.py
+- https://github.com/jgstew/jgstew-recipes/blob/main/SharedProcessors/BigFixActioner.py
+- https://github.com/jgstew/jgstew-recipes/blob/main/SharedProcessors/BigFixSessionRelevance.py
+
# Pyinstaller
- `pyinstaller --clean --collect-all besapi --onefile .\src\bescli\bescli.py`
diff --git a/examples/action_and_monitor.py b/examples/action_and_monitor.py
new file mode 100644
index 0000000..1182949
--- /dev/null
+++ b/examples/action_and_monitor.py
@@ -0,0 +1,361 @@
+"""
+Create an action from a fixlet or task xml bes file
+and monitor it's results for ~300 seconds
+
+requires `besapi`, install with command `pip install besapi`
+
+NOTE: this script requires besapi v3.3.3+ due to use of besapi.plugin_utilities
+
+Example Usage:
+python3 examples/action_and_monitor.py -c -vv --file './examples/content/TestEcho-Universal.bes'
+
+Inspect examples/action_and_monitor.log for results
+"""
+
+import logging
+import ntpath
+import os
+import platform
+import sys
+import time
+import typing
+
+import lxml.etree
+
+import besapi
+import besapi.plugin_utilities
+
+__version__ = "1.2.1"
+verbose = 0
+invoke_folder = None
+
+
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def get_action_combined_relevance(relevances: typing.List[str]):
+ """take array of ordered relevance clauses and return relevance string for action"""
+
+ relevance_combined = ""
+
+ if not relevances:
+ return "False"
+ if len(relevances) == 0:
+ return "False"
+ if len(relevances) == 1:
+ return relevances[0]
+ if len(relevances) > 0:
+ for clause in relevances:
+ if len(relevance_combined) == 0:
+ relevance_combined = clause
+ else:
+ relevance_combined = (
+ "( " + relevance_combined + " ) AND ( " + clause + " )"
+ )
+
+ return relevance_combined
+
+
+def get_target_xml(targets=""):
+ """get target xml based upon input
+
+ Input can be a single string:
+ - starts with "" if all computers should be targeted
+ - Otherwise will be interpreted as custom relevance
+
+ Input can be a single int:
+ - Single Computer ID Target
+
+ Input can be an array:
+ - Array of Strings: ComputerName
+ - Array of Integers: ComputerID
+ """
+ if targets is None or not targets:
+ logging.warning("No valid targeting found, will target no computers.")
+ # default if invalid:
+ return "False"
+
+ # if targets is int:
+ if isinstance(targets, int):
+ if targets == 0:
+ raise ValueError(
+ "Int 0 is not valid Computer ID, set targets to an array of strings of computer names or an array of ints of computer ids or custom relevance string or "
+ )
+ return f"{targets}"
+
+ # if targets is str:
+ if isinstance(targets, str):
+ # if targets string starts with "":
+ if targets.startswith(""):
+ if "false" in targets.lower():
+ # In my testing, false does not work correctly
+ return "False"
+ # return "false"
+ return "true"
+ # treat as custom relevance:
+ return f""
+
+ # if targets is array:
+ if isinstance(targets, list):
+ element_type = type(targets[0])
+ if element_type is int:
+ # array of computer ids
+ return (
+ ""
+ + "".join(map(str, targets))
+ + ""
+ )
+ if element_type is str:
+ # array of computer names
+ return (
+ ""
+ + "".join(targets)
+ + ""
+ )
+
+ logging.warning("No valid targeting found, will target no computers.")
+
+ # default if invalid:
+ return "False"
+
+
+def action_from_bes_file(bes_conn, file_path, targets=""):
+ """create action from bes file with fixlet or task"""
+ # default to empty string:
+ custom_relevance_xml = ""
+
+ tree = lxml.etree.parse(file_path)
+
+ # //BES/*[self::Task or self::Fixlet]/*[@id='elid']/name()
+ bes_type = str(
+ tree.xpath("//BES/*[self::Task or self::Fixlet or self::SingleAction]")[0].tag
+ )
+
+ logging.debug("BES Type: %s", bes_type)
+
+ title = tree.xpath(f"//BES/{bes_type}/Title/text()")[0]
+
+ logging.debug("Title: %s", title)
+
+ try:
+ actionscript = tree.xpath(
+ f"//BES/{bes_type}/DefaultAction/ActionScript/text()"
+ )[0]
+ except IndexError:
+ # handle SingleAction case:
+ actionscript = tree.xpath(f"//BES/{bes_type}/ActionScript/text()")[0]
+
+ logging.debug("ActionScript: %s", actionscript)
+
+ try:
+ if bes_type != "SingleAction":
+ success_criteria = tree.xpath(
+ f"//BES/{bes_type}/DefaultAction/SuccessCriteria/@Option"
+ )[0]
+ else:
+ success_criteria = tree.xpath(f"//BES/{bes_type}/SuccessCriteria/@Option")[
+ 0
+ ]
+ except IndexError:
+ # set success criteria if missing: (default)
+ success_criteria = "RunToCompletion"
+ if bes_type == "Fixlet":
+ # set success criteria if missing: (Fixlet)
+ success_criteria = "OriginalRelevance"
+
+ if success_criteria == "CustomRelevance":
+ if bes_type != "SingleAction":
+ custom_relevance = tree.xpath(
+ f"//BES/{bes_type}/DefaultAction/SuccessCriteria/text()"
+ )[0]
+ else:
+ custom_relevance = tree.xpath(f"//BES/{bes_type}/SuccessCriteria/text()")[0]
+
+ custom_relevance_xml = f""
+
+ logging.debug("success_criteria: %s", success_criteria)
+
+ relevance_clauses = tree.xpath(f"//BES/{bes_type}/Relevance/text()")
+
+ logging.debug("Relevances: %s", relevance_clauses)
+
+ relevance_clauses_combined = get_action_combined_relevance(relevance_clauses)
+
+ logging.debug("Relevance Combined: %s", relevance_clauses_combined)
+
+ action_xml = f"""
+
+
+ {title}
+
+
+ {custom_relevance_xml}
+
+ { get_target_xml(targets) }
+
+
+
+"""
+
+ logging.debug("Action XML:\n%s", action_xml)
+
+ action_result = bes_conn.post(bes_conn.url("actions"), data=action_xml)
+
+ logging.info("Action Result:/n%s", action_result)
+
+ action_id = action_result.besobj.Action.ID
+
+ logging.info("Action ID: %s", action_id)
+
+ return action_id
+
+
+def action_monitor_results(bes_conn, action_id, iterations=30, sleep_time=15):
+ """monitor the results of an action if interactive"""
+ previous_result = ""
+ i = 0
+ try:
+ # loop ~300 second for results
+ while i < iterations:
+ print("... waiting for results ... Ctrl+C to quit loop")
+
+ time.sleep(sleep_time)
+
+ # get the actual results:
+ # api/action/ACTION_ID/status?fields=ActionID,Status,DateIssued,DateStopped,StoppedBy,Computer(Status,State,StartTime)
+ # NOTE: this might not return anything if no clients have returned results
+ # this can be checked again and again for more results:
+ action_status_result = bes_conn.get(
+ bes_conn.url(
+ f"action/{action_id}/status?fields=ActionID,Status,DateIssued,DateStopped,StoppedBy,Computer(Status,State,StartTime)"
+ )
+ )
+
+ if previous_result != str(action_status_result):
+ logging.info(action_status_result)
+ previous_result = str(action_status_result)
+
+ i += 1
+
+ if action_status_result.besobj.ActionResults.Status == "Stopped":
+ logging.info("Action is stopped, halting monitoring loop")
+ break
+
+ # if not running interactively:
+ # https://stackoverflow.com/questions/2356399/tell-if-python-is-in-interactive-mode
+ if not sys.__stdin__.isatty():
+ logging.warning("not interactive, stopping loop")
+ break
+ except KeyboardInterrupt:
+ print("\nloop interuppted")
+
+ return previous_result
+
+
+def action_and_monitor(bes_conn, file_path, targets=""):
+ """Take action from bes xml file
+ monitor results of action"""
+
+ action_id = action_from_bes_file(bes_conn, file_path, targets)
+
+ logging.info("Start monitoring action results:")
+
+ results_action = action_monitor_results(bes_conn, action_id)
+
+ logging.info("End monitoring, Last Result:\n%s", results_action)
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ print("NOTE: this script requires besapi v3.3.3+ due to besapi.plugin_utilities")
+
+ parser = besapi.plugin_utilities.setup_plugin_argparse()
+
+ # add additonal arg specific to this script:
+ parser.add_argument(
+ "-f",
+ "--file",
+ help="xml bes file to create an action from",
+ required=False,
+ type=str,
+ )
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ print(log_file_path)
+
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config(
+ log_file_path, verbose, args.console
+ )
+
+ logging.basicConfig(**logging_config)
+
+ logging.info("---------- Starting New Session -----------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("%s's version: %s", get_invoke_file_name(verbose), __version__)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("Python version: %s", platform.sys.version)
+
+ bes_conn = besapi.plugin_utilities.get_besapi_connection(args)
+
+ # set targeting criteria to computer id int or "" or array
+ targets = 0
+
+ action_and_monitor(bes_conn, args.file, targets)
+
+ logging.info("---------- END -----------")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/baseline_by_relevance.py b/examples/baseline_by_relevance.py
new file mode 100644
index 0000000..0b16634
--- /dev/null
+++ b/examples/baseline_by_relevance.py
@@ -0,0 +1,121 @@
+"""
+create baseline by session relevance result
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import datetime
+import os
+
+import besapi
+
+# This relevance string must start with `fixlets` and return the set of fixlets you wish to turn into a baseline
+FIXLET_RELEVANCE = 'fixlets whose(name of it starts with "Update:") of bes sites whose( external site flag of it AND name of it = "Updates for Windows Applications Extended" )'
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(bes_conn.last_connected)
+
+ # change the relevance here to adjust which content gets put in a baseline:
+ fixlets_rel = FIXLET_RELEVANCE
+
+ # this gets the info needed from the items to make the baseline:
+ session_relevance = f"""(it as string) of (url of site of it, ids of it, content id of default action of it | "Action1") of it whose(exists default action of it AND globally visible flag of it AND name of it does not contain "(Superseded)" AND exists applicable computers whose(now - last report time of it < 60 * day) of it) of {fixlets_rel}"""
+
+ print("getting items to add to baseline...")
+ result = bes_conn.session_relevance_array(session_relevance)
+ print(f"{len(result)} items found")
+
+ # print(result)
+
+ baseline_components = ""
+
+ for item in result:
+ # print(item)
+ tuple_items = item.split(", ")
+ try:
+ baseline_components += f"""
+ """
+ except IndexError:
+ print("ERROR: a component was missing a key item.")
+ continue
+
+ # print(baseline_components)
+
+ # generate XML for baseline with template:
+ baseline = f"""
+
+
+ Custom Patching Baseline {datetime.datetime.today().strftime('%Y-%m-%d')}
+
+ true
+
+ {baseline_components}
+
+
+
+"""
+
+ # print(baseline)
+
+ file_path = "tmp_baseline.bes"
+ site_name = "Demo"
+ site_path = f"custom/{site_name}"
+
+ # Does not work through console import:
+ with open(file_path, "w") as f:
+ f.write(baseline)
+
+ print("Importing generated baseline...")
+ import_result = bes_conn.import_bes_to_site(file_path, site_path)
+
+ print(import_result)
+
+ os.remove(file_path)
+
+ # to automatically create an offer action, comment out the next line:
+ return True
+
+ baseline_id = import_result.besobj.Baseline.ID
+
+ print("creating baseline offer action...")
+
+ BES_SourcedFixletAction = f"""\
+
+
+
+ {site_name}
+ {baseline_id}
+ Action1
+
+
+ true
+
+
+ true
+ P10D
+ true
+
+ true
+ false
+ Testing
+
+
+
+
+"""
+
+ action_result = bes_conn.post("actions", BES_SourcedFixletAction)
+
+ print(action_result)
+
+ print("Finished!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/baseline_plugin.config.yaml b/examples/baseline_plugin.config.yaml
new file mode 100644
index 0000000..ea6f5dd
--- /dev/null
+++ b/examples/baseline_plugin.config.yaml
@@ -0,0 +1,9 @@
+---
+bigfix:
+ content:
+ Baselines:
+ automation:
+ trigger_file_path: baseline_plugin_run_now
+ sites:
+ - name: Updates for Windows Applications Extended
+ - name: Updates for Windows Applications
diff --git a/examples/baseline_plugin.py b/examples/baseline_plugin.py
new file mode 100644
index 0000000..03d6058
--- /dev/null
+++ b/examples/baseline_plugin.py
@@ -0,0 +1,308 @@
+"""
+Generate patching baselines from sites
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python baseline_plugin.py -r https://localhost:52311/api -u API_USER -p API_PASSWORD
+
+References:
+- https://github.com/jgstew/besapi/blob/master/examples/rest_cmd_args.py
+- https://github.com/jgstew/besapi/blob/master/examples/baseline_by_relevance.py
+- https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+"""
+
+import argparse
+import datetime
+import getpass
+import logging
+import logging.handlers
+import os
+import platform
+import sys
+
+import ruamel.yaml
+
+import besapi
+
+__version__ = "0.0.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+config_yaml = None
+
+
+def get_invoke_folder():
+ """Get the folder the script was invoked from
+
+ References:
+ - https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+ """
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_config(path="baseline_plugin.config.yaml"):
+ """load config from yaml file"""
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ path = os.path.join(invoke_folder, path)
+
+ logging.info("loading config from: `%s`", path)
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ raise FileNotFoundError(path)
+
+ with open(path, "r", encoding="utf-8") as stream:
+ yaml = ruamel.yaml.YAML(typ="safe", pure=True)
+ config_yaml = yaml.load(stream)
+
+ if verbose > 1:
+ logging.debug(config_yaml["bigfix"])
+
+ return config_yaml
+
+
+def test_file_exists(path):
+ """return true if file exists"""
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ path = os.path.join(invoke_folder, path)
+
+ logging.info("testing if exists: `%s`", path)
+
+ if os.path.isfile(path) and os.access(path, os.R_OK) and os.access(path, os.W_OK):
+ return path
+
+ return False
+
+
+def create_baseline_from_site(site):
+ """create a patching baseline from a site name
+
+ References:
+ - https://github.com/jgstew/besapi/blob/master/examples/baseline_by_relevance.py"""
+
+ site_name = site["name"]
+ logging.info("Create patching baseline for site: %s", site_name)
+
+ # Example:
+ # fixlets of bes sites whose(exists (it as trimmed string as lowercase) whose(it = "Updates for Windows Applications Extended" as trimmed string as lowercase) of (display names of it; names of it))
+ fixlets_rel = f'fixlets of bes sites whose(exists (it as trimmed string as lowercase) whose(it = "{site_name}" as trimmed string as lowercase) of (display names of it; names of it))'
+
+ session_relevance = f"""(it as string) of (url of site of it, ids of it, content id of default action of it | "Action1") of it whose(exists default action of it AND globally visible flag of it AND name of it does not contain "(Superseded)" AND exists applicable computers whose(now - last report time of it < 60 * day) of it) of {fixlets_rel}"""
+
+ result = bes_conn.session_relevance_array(session_relevance)
+
+ num_items = len(result)
+
+ if num_items > 1:
+ logging.info("Number of items to add to baseline: %s", num_items)
+
+ baseline_components = ""
+
+ IncludeInRelevance = "true"
+
+ fixlet_ids_str = "0"
+
+ if num_items > 100:
+ IncludeInRelevance = "false"
+
+ for item in result:
+ tuple_items = item.split(", ")
+ fixlet_ids_str += " ; " + tuple_items[1]
+ baseline_components += f"""
+ """
+
+ logging.debug(baseline_components)
+
+ # only have the baseline be relevant for 60 days after creation:
+ baseline_rel = f'exists absolute values whose(it < 60 * day) of (current date - "{ datetime.datetime.today().strftime("%d %b %Y") }" as date)'
+
+ if num_items > 100:
+ site_rel_query = f"""unique value of site level relevances of bes sites whose(exists (it as trimmed string as lowercase) whose(it = "{site_name}" as trimmed string as lowercase) of (display names of it; names of it))"""
+ site_rel = bes_conn.session_relevance_string(site_rel_query)
+
+ baseline_rel = f"""( {baseline_rel} ) AND ( {site_rel} )"""
+
+ # # This does not appear to work as expected:
+ # # create baseline relevance such that only relevant if 1+ fixlet is relevant
+ # if num_items > 100:
+ # baseline_rel = f"""exists relevant fixlets whose(id of it is contained by set of ({ fixlet_ids_str })) of sites whose("Fixlet Site" = type of it AND "{ site_name }" = name of it)"""
+
+ # generate XML for baseline with template:
+ baseline_xml = f"""
+
+
+ Patches from {site_name} - {datetime.datetime.today().strftime('%Y-%m-%d')}
+
+
+ PT12H
+
+ {baseline_components}
+
+
+
+ """
+
+ logging.debug("Baseline XML:\n%s", baseline_xml)
+
+ file_path = "tmp_baseline.bes"
+
+ # the custom site to import the baseline into:
+ import_site_name = "Demo"
+ site_path = f"custom/{import_site_name}"
+
+ # Does not work through console import:
+ with open(file_path, "w", encoding="utf-8") as f:
+ f.write(baseline_xml)
+
+ logging.info("Importing generated baseline for %s ...", site_name)
+ import_result = bes_conn.import_bes_to_site(file_path, site_path)
+
+ logging.info("Result: Import XML:\n%s", import_result)
+
+ os.remove(file_path)
+
+
+def process_baselines(config):
+ """generate baselines"""
+
+ for site in config:
+ create_baseline_from_site(site)
+
+
+def main():
+ """Execution starts here"""
+ print("main() start")
+
+ parser = argparse.ArgumentParser(
+ description="Provde command line arguments for REST URL, username, and password"
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ help="Set verbose output",
+ required=False,
+ action="count",
+ default=0,
+ )
+ parser.add_argument(
+ "-besserver", "--besserver", help="Specify the BES URL", required=False
+ )
+ parser.add_argument("-r", "--rest-url", help="Specify the REST URL", required=False)
+ parser.add_argument("-u", "--user", help="Specify the username", required=False)
+ parser.add_argument("-p", "--password", help="Specify the password", required=False)
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, config_yaml, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ # set different log levels:
+ log_level = logging.INFO
+ if verbose:
+ log_level = logging.INFO
+ if verbose > 1:
+ log_level = logging.DEBUG
+
+ # get path to put log file in:
+ log_filename = os.path.join(invoke_folder, "baseline_plugin.log")
+
+ print(f"Log File Path: {log_filename}")
+
+ handlers = [
+ logging.handlers.RotatingFileHandler(
+ log_filename, maxBytes=5 * 1024 * 1024, backupCount=1
+ )
+ ]
+
+ # log output to console if arg provided:
+ if verbose:
+ handlers.append(logging.StreamHandler())
+
+ # setup logging:
+ logging.basicConfig(
+ encoding="utf-8",
+ level=log_level,
+ format="%(asctime)s %(levelname)s:%(message)s",
+ handlers=handlers,
+ )
+ logging.info("----- Starting New Session ------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("Python version: %s", platform.sys.version)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("this plugin's version: %s", __version__)
+
+ password = args.password
+
+ if not password:
+ logging.warning("Password was not provided, provide REST API password.")
+ print("Password was not provided, provide REST API password.")
+ password = getpass.getpass()
+
+ # process args, setup connection:
+ rest_url = args.rest_url
+
+ # normalize url to https://HostOrIP:52311
+ if rest_url and rest_url.endswith("/api"):
+ rest_url = rest_url.replace("/api", "")
+
+ try:
+ bes_conn = besapi.besapi.BESConnection(args.user, password, rest_url)
+ # bes_conn.login()
+ except (
+ AttributeError,
+ ConnectionRefusedError,
+ besapi.besapi.requests.exceptions.ConnectionError,
+ ):
+ try:
+ # print(args.besserver)
+ bes_conn = besapi.besapi.BESConnection(args.user, password, args.besserver)
+ # handle case where args.besserver is None
+ # AttributeError: 'NoneType' object has no attribute 'startswith'
+ except AttributeError:
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+
+ # get config:
+ config_yaml = get_config()
+
+ trigger_path = config_yaml["bigfix"]["content"]["Baselines"]["automation"][
+ "trigger_file_path"
+ ]
+
+ # check if file exists, if so, return path, else return false:
+ trigger_path = test_file_exists(trigger_path)
+
+ if trigger_path:
+ process_baselines(
+ config_yaml["bigfix"]["content"]["Baselines"]["automation"]["sites"]
+ )
+ # delete trigger file
+ os.remove(trigger_path)
+ else:
+ logging.info("Trigger File Does Not Exists, skipping execution!")
+
+ logging.info("----- Ending Session ------")
+ print("main() End")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/client_query_from_string.py b/examples/client_query_from_string.py
new file mode 100644
index 0000000..5d489f9
--- /dev/null
+++ b/examples/client_query_from_string.py
@@ -0,0 +1,193 @@
+"""
+Example session relevance results from a string
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import json
+import logging
+import ntpath
+import os
+import platform
+import sys
+import time
+
+import besapi
+import besapi.plugin_utilities
+
+CLIENT_RELEVANCE = "(computer names, model name of main processor, (it as string) of (it / (1024 * 1024 * 1024)) of total amount of ram)"
+__version__ = "1.0.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+
+
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ print("NOTE: this script requires besapi v3.3.3+ due to besapi.plugin_utilities")
+
+ parser = besapi.plugin_utilities.setup_plugin_argparse()
+
+ # add additonal arg specific to this script:
+ parser.add_argument(
+ "-q",
+ "--query",
+ help="client query relevance",
+ required=False,
+ type=str,
+ default=CLIENT_RELEVANCE,
+ )
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ print(log_file_path)
+
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config(
+ log_file_path, verbose, args.console
+ )
+
+ logging.basicConfig(**logging_config)
+
+ logging.info("---------- Starting New Session -----------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("Python version: %s", platform.sys.version)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("this plugin's version: %s", __version__)
+
+ bes_conn = besapi.plugin_utilities.get_besapi_connection(args)
+
+ # get the ~10 most recent computers to report into BigFix:
+ session_relevance = 'tuple string items (integers in (0,9)) of concatenations ", " of (it as string) of ids of bes computers whose(now - last report time of it < 25 * minute)'
+
+ data = {"output": "json", "relevance": session_relevance}
+
+ # submitting session relevance query using POST to reduce problems:
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ json_result = json.loads(str(result))
+
+ # json_string = json.dumps(json_result, indent=2)
+ # print(json_string)
+
+ # for item in json_result["result"]:
+ # print(item)
+
+ # this is the client relevance we are going to get the results of:
+ client_relevance = args.query
+
+ # generate target XML substring from list of computer ids:
+ target_xml = (
+ ""
+ + "".join(json_result["result"])
+ + ""
+ )
+
+ # python template for ClientQuery BESAPI XML:
+ query_payload = f"""
+
+ true
+ {client_relevance}
+
+ {target_xml}
+
+
+"""
+
+ # print(query_payload)
+
+ # send the client query: (need it's ID to get results)
+ query_submit_result = bes_conn.post(bes_conn.url("clientquery"), data=query_payload)
+
+ # print(query_submit_result)
+ # print(query_submit_result.besobj.ClientQuery.ID)
+
+ previous_result = ""
+ i = 0
+ try:
+ # loop ~90 second for results
+ while i < 9:
+ print("... waiting for results ... Ctrl+C to quit loop")
+
+ # TODO: loop this to keep getting more results until all return or any key pressed
+ time.sleep(10)
+
+ # get the actual results:
+ # NOTE: this might not return anything if no clients have returned results
+ # this can be checked again and again for more results:
+ query_result = bes_conn.get(
+ bes_conn.url(
+ f"clientqueryresults/{query_submit_result.besobj.ClientQuery.ID}"
+ )
+ )
+
+ if previous_result != str(query_result):
+ print(query_result)
+ previous_result = str(query_result)
+
+ i += 1
+
+ # if not running interactively:
+ # https://stackoverflow.com/questions/2356399/tell-if-python-is-in-interactive-mode
+ if not sys.__stdin__.isatty():
+ print("not interactive, stopping loop")
+ break
+ except KeyboardInterrupt:
+ print("\nloop interuppted")
+
+ print("script finished")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/computers_delete_by_file.py b/examples/computers_delete_by_file.py
new file mode 100644
index 0000000..8542fc2
--- /dev/null
+++ b/examples/computers_delete_by_file.py
@@ -0,0 +1,58 @@
+"""
+Delete computers in file
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import os
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ # get the directory this script is running within:
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+ # get the file "computers_delete_by_file.txt" within the folder of the script:
+ comp_file_path = os.path.join(script_dir, "computers_delete_by_file.txt")
+
+ comp_file_lines = []
+ with open(comp_file_path, "r") as comp_file:
+ for line in comp_file:
+ line = line.strip()
+ if line != "":
+ comp_file_lines.append(line)
+
+ # print(comp_file_lines)
+
+ computers = '"' + '";"'.join(comp_file_lines) + '"'
+
+ # by default, this will only return computers that have not reported in >90 days:
+ session_relevance = f"unique values of ids of bes computers whose(now - last report time of it > 90 * day AND exists elements of intersections of (it; sets of ({computers})) of sets of (name of it; id of it as string))"
+
+ # get session relevance result of computer ids from list of computer ids or computer names:
+ results = bes_conn.session_relevance_array(session_relevance)
+
+ # print(results)
+
+ if "Nothing returned, but no error." in results[0]:
+ print("WARNING: No computers found to delete!")
+ return None
+
+ # delete computers:
+ for item in results:
+ if item.strip() != "":
+ computer_id = str(int(item))
+ print(f"INFO: Attempting to delete Computer ID: {computer_id}")
+ result_del = bes_conn.delete(bes_conn.url(f"computer/{computer_id}"))
+ if "ok" not in result_del.text:
+ print(f"ERROR: {result_del} for id: {computer_id}")
+ continue
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/computers_delete_by_file.txt b/examples/computers_delete_by_file.txt
new file mode 100644
index 0000000..c59cc26
--- /dev/null
+++ b/examples/computers_delete_by_file.txt
@@ -0,0 +1,2 @@
+7cf29da417f9
+15083644
diff --git a/examples/content/RelaySelectAction.bes b/examples/content/RelaySelectAction.bes
new file mode 100644
index 0000000..e202889
--- /dev/null
+++ b/examples/content/RelaySelectAction.bes
@@ -0,0 +1,10 @@
+
+
+
+ Relay Select
+
+ relay select
+
+ false
+
+
diff --git a/examples/content/RelaySelectTask.bes b/examples/content/RelaySelectTask.bes
new file mode 100644
index 0000000..78d2117
--- /dev/null
+++ b/examples/content/RelaySelectTask.bes
@@ -0,0 +1,31 @@
+
+
+
+ RelaySelect
+
+ not exists relay service
+ not exists main gather service
+
+
+ Internal
+ jgstew
+ 2021-08-03
+
+
+
+
+ x-fixlet-modification-time
+ Tue, 03 Aug 2021 15:18:27 +0000
+
+ BESC
+
+
+ Click
+ here
+ to deploy this action.
+
+
+
+
+
diff --git a/examples/content/RelaySetAffiliationGroup.bes b/examples/content/RelaySetAffiliationGroup.bes
new file mode 100644
index 0000000..7d91058
--- /dev/null
+++ b/examples/content/RelaySetAffiliationGroup.bes
@@ -0,0 +1,32 @@
+
+
+
+ set relay affiliation group
+
+ exists relay service
+ not exists main gather service
+ not exists settings "_BESRelay_Register_Affiliation_AdvertisementList" of client
+
+ Internal
+ jgstew
+
+
+
+
+
+ x-fixlet-modification-time
+ Tue, 03 Aug 2021 15:18:27 +0000
+
+ BESC
+
+
+ Click
+ here
+ to deploy this action.
+
+
+
+
+
diff --git a/examples/content/RelaySetNameOverride.bes b/examples/content/RelaySetNameOverride.bes
new file mode 100644
index 0000000..b9ab958
--- /dev/null
+++ b/examples/content/RelaySetNameOverride.bes
@@ -0,0 +1,32 @@
+
+
+
+ set relay name override
+
+ exists relay service
+ not exists main gather service
+ not exists settings "_BESClient_Relay_NameOverride" of client
+
+ Internal
+ jgstew
+
+
+
+
+
+ x-fixlet-modification-time
+ Tue, 03 Aug 2021 15:18:27 +0000
+
+ BESC
+
+
+ Click
+ here
+ to deploy this action.
+
+
+
+
+
diff --git a/examples/content/TestEcho-Universal.bes b/examples/content/TestEcho-Universal.bes
new file mode 100644
index 0000000..21e94a1
--- /dev/null
+++ b/examples/content/TestEcho-Universal.bes
@@ -0,0 +1,30 @@
+
+
+
+ test echo - all
+
+
+
+ Internal
+
+ 2021-08-03
+
+
+
+
+ x-fixlet-modification-time
+ Tue, 03 Aug 2021 15:18:27 +0000
+
+ BESC
+
+
+ Click
+ here
+ to deploy this action.
+
+ {(concatenations (if windows of operating system then "^ " else "\ ") of substrings separated by " " of it) of pathname of folders "Logs" of folders "__Global" of data folders of client}{if windows of operating system then "\" else "/"}test_echo.log"
+]]>
+
+
+
diff --git a/examples/dashboard_variable_get_value.py b/examples/dashboard_variable_get_value.py
new file mode 100644
index 0000000..56effa2
--- /dev/null
+++ b/examples/dashboard_variable_get_value.py
@@ -0,0 +1,38 @@
+"""
+get dashboard variable value
+
+requires `besapi` v3.2.6+
+
+install with command `pip install -U besapi`
+"""
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(bes_conn.last_connected)
+
+ print(bes_conn.get_dashboard_variable_value("WebUIAppAdmin", "Current_Sites"))
+
+ # dashboard_name = "PyBESAPITest"
+ # var_name = "TestVar"
+
+ # print(
+ # bes_conn.set_dashboard_variable_value(
+ # dashboard_name, var_name, "dashboard_variable_get_value.py 12345678"
+ # )
+ # )
+
+ # print(bes_conn.get_dashboard_variable_value(dashboard_name, var_name))
+
+ # print(bes_conn.delete(f"dashboardvariable/{dashboard_name}/{var_name}"))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/delete_task_by_id.py b/examples/delete_task_by_id.py
new file mode 100644
index 0000000..000dce6
--- /dev/null
+++ b/examples/delete_task_by_id.py
@@ -0,0 +1,30 @@
+"""
+delete tasks by id
+- https://developer.bigfix.com/rest-api/api/task.html
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ ids = [0, "0"]
+
+ # https://developer.bigfix.com/rest-api/api/task.html
+ # task/{site type}/{site name}/{task id}
+
+ for id in ids:
+ rest_url = f"task/custom/CUSTOM_SITE_NAME/{int(id)}"
+ print(f"Deleting: {rest_url}")
+ result = bes_conn.delete(rest_url)
+ print(result.text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/export_all_sites.py b/examples/export_all_sites.py
new file mode 100644
index 0000000..b98713a
--- /dev/null
+++ b/examples/export_all_sites.py
@@ -0,0 +1,138 @@
+"""
+This will export all bigfix sites to a folder called `export`
+
+This is equivalent of running `python -m besapi export_all_sites`
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python export_all_sites.py -r https://localhost:52311/api -u API_USER -p API_PASSWORD
+
+References:
+- https://developer.bigfix.com/rest-api/api/admin.html
+- https://github.com/jgstew/besapi/blob/master/examples/rest_cmd_args.py
+- https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+"""
+
+import argparse
+import getpass
+import logging
+import logging.handlers
+import ntpath
+import os
+import platform
+import shutil
+import sys
+
+import besapi
+import besapi.plugin_utilities
+
+__version__ = "1.1.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+
+
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def main():
+ """Execution starts here"""
+ print("main() start")
+
+ print("NOTE: this script requires besapi v3.3.3+")
+
+ parser = besapi.plugin_utilities.setup_plugin_argparse()
+
+ # add additonal arg specific to this script:
+ parser.add_argument(
+ "-d",
+ "--delete",
+ help="delete previous export",
+ required=False,
+ action="store_true",
+ )
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ print(log_file_path)
+
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config(
+ log_file_path, verbose, args.console
+ )
+
+ logging.basicConfig(**logging_config)
+
+ logging.info("----- Starting New Session ------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("Python version: %s", platform.sys.version)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("this plugin's version: %s", __version__)
+
+ bes_conn = besapi.plugin_utilities.get_besapi_connection(args)
+
+ export_folder = os.path.join(invoke_folder, "export")
+
+ # if --delete arg used, delete export folder:
+ if args.delete:
+ shutil.rmtree(export_folder, ignore_errors=True)
+
+ try:
+ os.mkdir(export_folder)
+ except FileExistsError:
+ logging.warning("Folder already exists!")
+
+ os.chdir(export_folder)
+
+ bes_conn.export_all_sites()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/export_bes_by_relevance.py b/examples/export_bes_by_relevance.py
new file mode 100644
index 0000000..9d959c8
--- /dev/null
+++ b/examples/export_bes_by_relevance.py
@@ -0,0 +1,40 @@
+"""
+Example export bes files by session relevance result
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import time
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(bes_conn.last_connected)
+
+ # change the relevance here to adjust which content gets exported:
+ fixlets_rel = 'custom bes fixlets whose(name of it as lowercase contains "oracle")'
+
+ # this does not currently work with things in the actionsite:
+ session_relevance = f'(type of it as lowercase & "/custom/" & name of site of it & "/" & id of it as string) of {fixlets_rel}'
+
+ result = bes_conn.session_relevance_array(session_relevance)
+
+ for item in result:
+ print(item)
+ # export bes file:
+ print(bes_conn.export_item_by_resource(item, "./tmp/"))
+
+
+if __name__ == "__main__":
+ # Start the timer
+ start_time = time.time()
+ main()
+ # Calculate the elapsed time
+ elapsed_time = time.time() - start_time
+ print(f"Execution time: {elapsed_time:.2f} seconds")
diff --git a/examples/export_bes_by_relevance_async.py b/examples/export_bes_by_relevance_async.py
new file mode 100644
index 0000000..a2cd8d3
--- /dev/null
+++ b/examples/export_bes_by_relevance_async.py
@@ -0,0 +1,131 @@
+"""
+Example export bes files by session relevance result
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import asyncio
+import configparser
+import os
+import time
+
+import aiofiles
+import aiohttp
+
+import besapi
+
+
+def get_bes_pass_using_config_file(conf_file=None):
+ """
+ read connection values from config file
+ return besapi connection
+ """
+ config_paths = [
+ "/etc/besapi.conf",
+ os.path.expanduser("~/besapi.conf"),
+ os.path.expanduser("~/.besapi.conf"),
+ "besapi.conf",
+ ]
+ # if conf_file specified, then only use that:
+ if conf_file:
+ config_paths = [conf_file]
+
+ configparser_instance = configparser.ConfigParser()
+
+ found_config_files = configparser_instance.read(config_paths)
+
+ if found_config_files and configparser_instance:
+ print("Attempting BESAPI Connection using config file:", found_config_files)
+
+ try:
+ BES_PASSWORD = configparser_instance.get("besapi", "BES_PASSWORD")
+ except BaseException: # pylint: disable=broad-except
+ BES_PASSWORD = None
+
+ return BES_PASSWORD
+
+
+async def fetch(session, url):
+ """get items async"""
+ async with session.get(url) as response:
+ response_text = await response.text()
+
+ # Extract the filename from the URL
+ url_parts = url.split("/")
+
+ file_dir = "./tmp/" + url_parts[-2] + "/" + url_parts[-4]
+
+ os.makedirs(file_dir, exist_ok=True)
+
+ filename = file_dir + "/" + url_parts[-1] + ".bes"
+
+ # Write the response to a file asynchronously
+ async with aiofiles.open(filename, "w") as file:
+ await file.write(response_text)
+
+ print(f"{filename} downloaded and saved.")
+
+
+async def main():
+ """Execution starts here"""
+ print("main()")
+
+ # Create a semaphore with a maximum concurrent requests
+ semaphore = asyncio.Semaphore(3)
+
+ # TODO: get max mod time of existing bes files:
+ # https://github.com/jgstew/tools/blob/master/Python/get_max_time_bes_files.py
+
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(bes_conn.last_connected)
+
+ # change the relevance here to adjust which content gets exported:
+ fixlets_rel = 'custom bes fixlets whose(name of it as lowercase contains "oracle")'
+
+ # this does not currently work with things in the actionsite:
+ session_relevance = f'(type of it as lowercase & "/custom/" & name of site of it & "/" & id of it as string) of {fixlets_rel}'
+
+ result = bes_conn.session_relevance_array(session_relevance)
+
+ print(f"{len(result)} items to export...")
+
+ absolute_urls = []
+
+ for item in result:
+ absolute_urls.append(bes_conn.url(item))
+
+ # Create a session for making HTTP requests
+ async with aiohttp.ClientSession(
+ auth=aiohttp.BasicAuth(bes_conn.username, get_bes_pass_using_config_file()),
+ connector=aiohttp.TCPConnector(ssl=False),
+ ) as session:
+ # Define a list of URLs to fetch
+ urls = absolute_urls
+
+ # Create a list to store the coroutines for fetching the URLs
+ tasks = []
+
+ # Create coroutines for fetching each URL
+ for url in urls:
+ # Acquire the semaphore before starting the request
+ async with semaphore:
+ task = asyncio.ensure_future(fetch(session, url))
+ tasks.append(task)
+
+ # Wait for all the coroutines to complete
+ await asyncio.gather(*tasks)
+
+
+if __name__ == "__main__":
+ # Start the timer
+ start_time = time.time()
+
+ # Run the main function
+ loop = asyncio.get_event_loop()
+ loop.run_until_complete(main())
+
+ # Calculate the elapsed time
+ elapsed_time = time.time() - start_time
+ print(f"Execution time: {elapsed_time:.2f} seconds")
diff --git a/examples/export_bes_by_relevance_threads.py b/examples/export_bes_by_relevance_threads.py
new file mode 100644
index 0000000..20778bb
--- /dev/null
+++ b/examples/export_bes_by_relevance_threads.py
@@ -0,0 +1,44 @@
+"""
+Example export bes files by session relevance result
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import concurrent.futures
+import time
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(bes_conn.last_connected)
+
+ # change the relevance here to adjust which content gets exported:
+ fixlets_rel = 'custom bes fixlets whose(name of it as lowercase contains "oracle")'
+
+ # this does not currently work with things in the actionsite:
+ session_relevance = f'(type of it as lowercase & "/custom/" & name of site of it & "/" & id of it as string) of {fixlets_rel}'
+
+ result = bes_conn.session_relevance_array(session_relevance)
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
+ futures = [
+ executor.submit(bes_conn.export_item_by_resource, item, "./tmp/")
+ for item in result
+ ]
+ # Wait for all tasks to complete
+ concurrent.futures.wait(futures)
+
+
+if __name__ == "__main__":
+ # Start the timer
+ start_time = time.time()
+ main()
+ # Calculate the elapsed time
+ elapsed_time = time.time() - start_time
+ print(f"Execution time: {elapsed_time:.2f} seconds")
diff --git a/examples/fixlet_add_mime_field.py b/examples/fixlet_add_mime_field.py
new file mode 100644
index 0000000..cafe104
--- /dev/null
+++ b/examples/fixlet_add_mime_field.py
@@ -0,0 +1,92 @@
+"""
+Add mime field to custom content
+
+Need to url escape site name https://bigfix:52311/api/sites
+"""
+
+import lxml.etree
+
+import besapi
+
+FIXLET_NAME = "Install Microsoft Orca from local SDK - Windows"
+MIME_FIELD = "x-relevance-evaluation-period"
+session_relevance = (
+ 'custom bes fixlets whose(name of it = "'
+ + FIXLET_NAME
+ + '" AND not exists mime fields "'
+ + MIME_FIELD
+ + '" of it)'
+)
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ data = {"relevance": "id of " + session_relevance}
+
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ # example result: fixlet/custom/Public%2fWindows/21405
+ # full example: https://bigfix:52311/api/fixlet/custom/Public%2fWindows/21405
+ fixlet_id = int(result.besdict["Query"]["Result"]["Answer"])
+
+ print(fixlet_id)
+
+ data = {"relevance": "name of site of " + session_relevance}
+
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ fixlet_site_name = str(result.besdict["Query"]["Result"]["Answer"])
+
+ # escape `/` in site name, if applicable
+ # do spaces need escaped too? `%20`
+ fixlet_site_name = fixlet_site_name.replace("/", "%2f")
+
+ print(fixlet_site_name)
+
+ fixlet_content = bes_conn.get_content_by_resource(
+ f"fixlet/custom/{fixlet_site_name}/{fixlet_id}"
+ )
+
+ # print(fixlet_content)
+
+ root_xml = lxml.etree.fromstring(fixlet_content.besxml)
+
+ # get first MIMEField
+ xml_first_mime = root_xml.find(".//*/MIMEField")
+
+ xml_container = xml_first_mime.getparent()
+
+ print(lxml.etree.tostring(xml_first_mime))
+
+ print(xml_container.index(xml_first_mime))
+
+ # new mime to set relevance eval to once an hour:
+ new_mime = lxml.etree.XML(
+ """
+ x-relevance-evaluation-period
+ 01:00:00
+ """
+ )
+
+ print(lxml.etree.tostring(new_mime))
+
+ # insert new mime BEFORE first MIME
+ # https://stackoverflow.com/questions/7474972/append-element-after-another-element-using-lxml
+ xml_container.insert(xml_container.index(xml_first_mime) - 1, new_mime)
+
+ print(
+ "\nPreview of new XML:\n ",
+ lxml.etree.tostring(root_xml, encoding="utf-8", xml_declaration=True).decode(
+ "utf-8"
+ ),
+ )
+
+ # TODO: PUT changed XML back to RESTAPI resource to modify
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/get_upload.py b/examples/get_upload.py
new file mode 100644
index 0000000..6a278fc
--- /dev/null
+++ b/examples/get_upload.py
@@ -0,0 +1,30 @@
+"""
+Get existing upload info by sha1 and filename
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ result = bes_conn.get_upload(
+ "test_besapi_upload.txt", "092bd8ef7b91507bb3848640ef47bb392e7d95b1"
+ )
+
+ print(result)
+
+ if result:
+ print(bes_conn.parse_upload_result_to_prefetch(result))
+ print("Info: Upload found.")
+ else:
+ print("ERROR: Upload not found!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/import_bes_file.py b/examples/import_bes_file.py
new file mode 100644
index 0000000..f6abee4
--- /dev/null
+++ b/examples/import_bes_file.py
@@ -0,0 +1,35 @@
+"""
+import bes file into site
+
+- https://developer.bigfix.com/rest-api/api/import.html
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+SITE_PATH = "custom/demo"
+BES_FILE_PATH = "examples/example.bes"
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ print(f"besapi version: { besapi.__version__ }")
+
+ if not hasattr(besapi.besapi.BESConnection, "import_bes_to_site"):
+ print("version of besapi is too old, must be >= 3.1.6")
+ return None
+
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ # requires besapi 3.1.6
+ result = bes_conn.import_bes_to_site(BES_FILE_PATH, SITE_PATH)
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/import_bes_files.py b/examples/import_bes_files.py
new file mode 100644
index 0000000..1573bde
--- /dev/null
+++ b/examples/import_bes_files.py
@@ -0,0 +1,47 @@
+"""
+import bes file into site
+
+- https://developer.bigfix.com/rest-api/api/import.html
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import glob
+
+import besapi
+
+SITE_PATH = "custom/demo"
+
+# by default, get all BES files in examples folder:
+BES_FOLDER_GLOB = "./examples/*.bes"
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ print(f"besapi version: { besapi.__version__ }")
+
+ if not hasattr(besapi.besapi.BESConnection, "import_bes_to_site"):
+ print("version of besapi is too old, must be >= 3.1.6")
+ return None
+
+ files = glob.glob(BES_FOLDER_GLOB)
+
+ if len(files) > 0:
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+ else:
+ print(f"No BES Files found using glob: {BES_FOLDER_GLOB}")
+ return None
+
+ # import all found BES files into site:
+ for f in files:
+ print(f"Importing file: {f}")
+ # requires besapi 3.1.6
+ result = bes_conn.import_bes_to_site(f, SITE_PATH)
+ print(result)
+
+
+if __name__ == "__main__":
+ print(main())
diff --git a/examples/mailbox_files_create.py b/examples/mailbox_files_create.py
new file mode 100644
index 0000000..695b3a9
--- /dev/null
+++ b/examples/mailbox_files_create.py
@@ -0,0 +1,41 @@
+"""
+Get set of mailbox files
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import os
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ session_rel = 'tuple string items 0 of concatenations ", " of (it as string) of ids of bes computers whose(root server flag of it AND now - last report time of it < 30 * day)'
+
+ # get root server computer id:
+ root_id = int(bes_conn.session_relevance_string(session_rel).strip())
+
+ print(root_id)
+
+ file_path = "examples/mailbox_files_create.py"
+ file_name = os.path.basename(file_path)
+
+ # https://developer.bigfix.com/rest-api/api/mailbox.html
+
+ # Example Header:: Content-Disposition: attachment; filename="file.xml"
+ headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
+ with open(file_path, "rb") as f:
+ result = bes_conn.post(
+ bes_conn.url(f"mailbox/{root_id}"), data=f, headers=headers
+ )
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/mailbox_files_list.py b/examples/mailbox_files_list.py
new file mode 100644
index 0000000..e0b4f40
--- /dev/null
+++ b/examples/mailbox_files_list.py
@@ -0,0 +1,30 @@
+"""
+Get set of mailbox files
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ session_rel = 'tuple string items 0 of concatenations ", " of (it as string) of ids of bes computers whose(root server flag of it AND now - last report time of it < 30 * day)'
+
+ # get root server computer id:
+ root_id = int(bes_conn.session_relevance_string(session_rel).strip())
+
+ print(root_id)
+
+ # list mailbox files:
+ result = bes_conn.get(bes_conn.url(f"mailbox/{root_id}"))
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/parameters_secure_sourced_fixlet_action.py b/examples/parameters_secure_sourced_fixlet_action.py
new file mode 100644
index 0000000..9c9a05a
--- /dev/null
+++ b/examples/parameters_secure_sourced_fixlet_action.py
@@ -0,0 +1,56 @@
+"""
+Example sourced fixlet action with parameters
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+# reference: https://software.bigfix.com/download/bes/100/util/BES10.0.7.52.xsd
+# https://forum.bigfix.com/t/api-sourcedfixletaction-including-end-time/37117/2
+# https://forum.bigfix.com/t/secret-parameter-actions/38847/13
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ # SessionRelevance for root server id:
+ session_relevance = """
+ maxima of ids of bes computers
+ whose(root server flag of it AND now - last report time of it < 1 * day)
+ """
+
+ root_server_id = int(bes_conn.session_relevance_string(session_relevance))
+
+ CONTENT_XML = (
+ r"""
+
+
+
+ BES Support
+ 15
+ Action1
+
+
+ """
+ + str(root_server_id)
+ + r"""
+
+ test_value
+ test_secure_value
+ Test parameters - secure - SourcedFixletAction - BES Clients Have Incorrect Clock Time
+
+
+"""
+ )
+
+ result = bes_conn.post("actions", CONTENT_XML)
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/parameters_sourced_fixlet_action.py b/examples/parameters_sourced_fixlet_action.py
new file mode 100644
index 0000000..c0f3552
--- /dev/null
+++ b/examples/parameters_sourced_fixlet_action.py
@@ -0,0 +1,43 @@
+"""
+Example sourced fixlet action with parameters
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import besapi
+
+# reference: https://software.bigfix.com/download/bes/100/util/BES10.0.7.52.xsd
+# https://forum.bigfix.com/t/api-sourcedfixletaction-including-end-time/37117/2
+# https://forum.bigfix.com/t/secret-parameter-actions/38847/13
+
+CONTENT_XML = r"""
+
+
+
+ BES Support
+ 15
+ Action1
+
+
+ BIGFIX
+
+ test_value_demo
+ Test parameters - SourcedFixletAction - BES Clients Have Incorrect Clock Time
+
+
+"""
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ result = bes_conn.post("actions", CONTENT_XML)
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/relay_info.py b/examples/relay_info.py
new file mode 100644
index 0000000..e2e924b
--- /dev/null
+++ b/examples/relay_info.py
@@ -0,0 +1,185 @@
+"""
+This will get info about relays in the environment
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python relay_info.py -r https://localhost:52311/api -u API_USER --days 90 -p API_PASSWORD
+
+References:
+- https://developer.bigfix.com/rest-api/api/admin.html
+- https://github.com/jgstew/besapi/blob/master/examples/rest_cmd_args.py
+- https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+"""
+
+import json
+import logging
+import logging.handlers
+import ntpath
+import os
+import platform
+import shutil
+import sys
+
+import besapi
+import besapi.plugin_utilities
+
+__version__ = "1.1.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+
+
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def main():
+ """Execution starts here"""
+ print("main() start")
+
+ print("NOTE: this script requires besapi v3.3.3+ due to besapi.plugin_utilities")
+ print(
+ "WARNING: results may be incorrect if not run as a MO or an account without scope of all computers"
+ )
+
+ parser = besapi.plugin_utilities.setup_plugin_argparse()
+
+ # add additonal arg specific to this script:
+ parser.add_argument(
+ "-d",
+ "--days",
+ help="last report days to filter on",
+ required=False,
+ type=int,
+ default=900,
+ )
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ print(log_file_path)
+
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config(
+ log_file_path, verbose, args.console
+ )
+
+ logging.basicConfig(**logging_config)
+
+ logging.info("---------- Starting New Session -----------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("%s's version: %s", get_invoke_file_name(verbose), __version__)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("Python version: %s", platform.sys.version)
+
+ bes_conn = besapi.plugin_utilities.get_besapi_connection(args)
+
+ # defaults to 900 days:
+ last_report_days_filter = args.days
+
+ # get relay info:
+ session_relevance = f"""(multiplicity of it, it) of unique values of (it as string) of (relay selection method of it | "NoRelayMethod" , relay server of it | "NoRelayServer") of bes computers whose(now - last report time of it < {last_report_days_filter} * day)"""
+ results = bes_conn.session_relevance_string(session_relevance)
+
+ logging.info("Relay Info:\n%s", results)
+
+ session_relevance = f"""(multiplicity of it, it) of unique values of (it as string) of (relay selection method of it | "NoRelayMethod" , relay server of it | "NoRelayServer", relay hostname of it | "NoRelayHostname", id of it | 0) of bes computers whose(now - last report time of it < {last_report_days_filter} * day AND relay server flag of it)"""
+ results = bes_conn.session_relevance_string(session_relevance)
+
+ logging.info("Info on Relays:\n%s", results)
+
+ session_relevance = f"""unique values of values of client settings whose(name of it = "_BESClient_Relay_NameOverride") of bes computers whose(now - last report time of it < {last_report_days_filter} * day)"""
+ results = bes_conn.session_relevance_string(session_relevance)
+
+ logging.info("Relay name override values:\n%s", results)
+
+ session_relevance = f"""(multiplicity of it, it) of unique values of values of client settings whose(name of it = "_BESRelay_Register_Affiliation_AdvertisementList") of bes computers whose(now - last report time of it < {last_report_days_filter} * day)"""
+ results = bes_conn.session_relevance_string(session_relevance)
+
+ logging.info("Relay_Register_Affiliation values:\n%s", results)
+
+ session_relevance = f"""(multiplicity of it, it) of unique values of values of client settings whose(name of it = "_BESClient_Register_Affiliation_SeekList") of bes computers whose(now - last report time of it < {last_report_days_filter} * day)"""
+ results = bes_conn.session_relevance_string(session_relevance)
+
+ logging.info("Client_Register_Affiliation_Seek values:\n%s", results)
+
+ # this should require MO:
+ results = bes_conn.get("admin/masthead/parameters")
+
+ logging.info(
+ "masthead parameters:\n%s",
+ json.dumps(results.besdict["MastheadParameters"], indent=2),
+ )
+
+ # this should require MO:
+ results = bes_conn.get("admin/fields")
+
+ logging.info(
+ "Admin Fields:\n%s", json.dumps(results.besdict["AdminField"], indent=2)
+ )
+
+ # this should require MO:
+ results = bes_conn.get("admin/options")
+
+ logging.info(
+ "Admin Options:\n%s", json.dumps(results.besdict["SystemOptions"], indent=2)
+ )
+
+ # this should require MO:
+ results = bes_conn.get("admin/reports")
+
+ logging.info(
+ "Admin Report Options:\n%s",
+ json.dumps(results.besdict["ClientReports"], indent=2),
+ )
+
+ logging.info("---------- Ending Session -----------")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/rest_cmd_args.py b/examples/rest_cmd_args.py
new file mode 100644
index 0000000..283d30f
--- /dev/null
+++ b/examples/rest_cmd_args.py
@@ -0,0 +1,62 @@
+"""
+Example session relevance results from a string
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python rest_cmd_args.py -r https://localhost:52311/api -u API_USER -p API_PASSWORD
+"""
+
+import argparse
+import json
+import logging
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ parser = argparse.ArgumentParser(
+ description="Provde command line arguments for REST URL, username, and password"
+ )
+ parser.add_argument(
+ "-besserver", "--besserver", help="Specify the BES URL", required=False
+ )
+ parser.add_argument("-r", "--rest-url", help="Specify the REST URL", required=True)
+ parser.add_argument("-u", "--user", help="Specify the username", required=True)
+ parser.add_argument("-p", "--password", help="Specify the password", required=True)
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ rest_url = args.rest_url
+
+ # normalize url to https://HostOrIP:52311
+ if rest_url.endswith("/api"):
+ rest_url = rest_url.replace("/api", "")
+
+ try:
+ bes_conn = besapi.besapi.BESConnection(args.user, args.password, rest_url)
+ # bes_conn.login()
+ except (ConnectionRefusedError, besapi.besapi.requests.exceptions.ConnectionError):
+ # print(args.besserver)
+ bes_conn = besapi.besapi.BESConnection(args.user, args.password, args.besserver)
+
+ # get unique device OSes
+ session_relevance = 'unique values of (it as trimmed string) of (preceding text of last " (" of it | it) of operating systems of bes computers'
+
+ data = {"output": "json", "relevance": session_relevance}
+
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ json_result = json.loads(str(result))
+
+ json_string = json.dumps(json_result, indent=2)
+
+ print(json_string)
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.WARNING)
+ main()
diff --git a/examples/send_message_all_computers.py b/examples/send_message_all_computers.py
new file mode 100644
index 0000000..9cff3b3
--- /dev/null
+++ b/examples/send_message_all_computers.py
@@ -0,0 +1,116 @@
+"""
+This will send a BigFix UI message to ALL computers!
+"""
+
+import besapi
+
+MESSAGE_TITLE = """Test message from besapi"""
+MESSAGE = MESSAGE_TITLE
+
+CONTENT_XML = rf"""
+
+
+ Send Message: {MESSAGE_TITLE}
+ = ("3.1.0" as version)) of key "HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall" of x32 registry) else if (mac of operating system) then (exists application "BigFixSSA.app" whose (version of it >= "3.1.0")) else false) AND (exists line whose (it = "disableMessagesTab: false") of file (if (windows of operating system) then (pathname of parent folder of parent folder of client) & "\BigFix Self Service Application\resources\ssa.config" else "/Library/Application Support/BigFix/BigFixSSA/ssa.config"))]]>
+ //Nothing to do
+
+
+ {MESSAGE_TITLE}
+ true
+
+ {MESSAGE}
]]>
+ false
+ false
+ false
+ ForceToRun
+ Interval
+ P3D
+ false
+
+ false
+ false
+ false
+ false
+ false
+ false
+ NoRequirement
+ AllUsers
+ false
+ false
+ false
+ true
+ 3
+ false
+ false
+ false
+ false
+
+ false
+
+
+ false
+ false
+
+ false
+ false
+ false
+ false
+ false
+ false
+
+ false
+
+ false
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+ false
+ false
+ false
+ false
+ false
+
+ false
+ false
+ false
+ false
+
+ true
+
+ true
+
+
+ action-ui-metadata
+ {{"type":"notification","sender":"broadcast","expirationDays":3}}
+
+
+
+"""
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ result = bes_conn.post("actions", CONTENT_XML)
+
+ print(result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/serversettings.cfg b/examples/serversettings.cfg
new file mode 100644
index 0000000..f7729f8
--- /dev/null
+++ b/examples/serversettings.cfg
@@ -0,0 +1,11 @@
+# this file is modeled after the clientsettings.cfg but for bigfix server admin fields
+# see the script that uses this file here:
+# https://github.com/jgstew/besapi/blob/master/examples/serversettings.py
+passwordComplexityDescription=Passwords must contain 12 characters or more, both uppercase and lowercase letters, and at least 1 digit.
+passwordComplexityRegex=(?=.*[[:lower:]])(?=.*[[:upper:]])(?=.*[[:digit:]]).{12,}
+disableNmoManualGroups = 1
+includeSFIDsInBaselineActions= 1
+requireConfirmAction =1
+loginTimeoutSeconds=7200
+timeoutLockMinutes=345
+timeoutLogoutMinutes=360
diff --git a/examples/serversettings.py b/examples/serversettings.py
new file mode 100644
index 0000000..b2cf16b
--- /dev/null
+++ b/examples/serversettings.py
@@ -0,0 +1,247 @@
+"""
+Set server settings like clientsettings.cfg
+
+See example serversettings.cfg file here:
+- https://github.com/jgstew/besapi/blob/master/examples/serversettings.cfg
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python serversettings.py -r https://localhost:52311/api -u API_USER -p API_PASSWORD
+
+References:
+- https://developer.bigfix.com/rest-api/api/admin.html
+- https://github.com/jgstew/besapi/blob/master/examples/rest_cmd_args.py
+- https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+"""
+
+import argparse
+import configparser
+import getpass
+import logging
+import logging.handlers
+import os
+import platform
+import sys
+
+import besapi
+
+__version__ = "0.0.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+config_ini = None
+
+
+def get_invoke_folder():
+ """Get the folder the script was invoked from
+
+ References:
+ - https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+ """
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_config(path="serversettings.cfg"):
+ """load config from ini file"""
+
+ # example config: https://github.com/jgstew/besapi/blob/master/examples/serversettings.cfg
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ path = os.path.join(invoke_folder, path)
+
+ logging.info("loading config from: `%s`", path)
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ raise FileNotFoundError(path)
+
+ configparser_instance = configparser.ConfigParser()
+
+ try:
+ # try read config file with section headers:
+ with open(path) as stream:
+ configparser_instance.read_string(stream.read())
+ except configparser.MissingSectionHeaderError:
+ # if section header missing, add a fake one:
+ with open(path) as stream:
+ configparser_instance.read_string(
+ "[bigfix_server_admin_fields]\n" + stream.read()
+ )
+
+ config_ini = list(configparser_instance.items("bigfix_server_admin_fields"))
+
+ logging.debug(config_ini)
+
+ return config_ini
+
+
+def test_file_exists(path):
+ """return true if file exists"""
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ path = os.path.join(invoke_folder, path)
+
+ logging.info("testing if exists: `%s`", path)
+
+ if os.path.isfile(path) and os.access(path, os.R_OK) and os.access(path, os.W_OK):
+ return path
+
+ return False
+
+
+def get_settings_xml(config):
+ """turn config into settings xml"""
+
+ settings_xml = ""
+
+ for setting in config:
+ settings_xml += f"""\n
+\t{setting[0]}
+\t{setting[1]}
+"""
+
+ settings_xml = (
+ """"""
+ + settings_xml
+ + "\n"
+ )
+
+ return settings_xml
+
+
+def post_settings(settings_xml):
+ """post settings to server"""
+
+ return bes_conn.post("admin/fields", settings_xml)
+
+
+def main():
+ """Execution starts here"""
+ print("main() start")
+
+ parser = argparse.ArgumentParser(
+ description="Provde command line arguments for REST URL, username, and password"
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ help="Set verbose output",
+ required=False,
+ action="count",
+ default=0,
+ )
+ parser.add_argument(
+ "-besserver", "--besserver", help="Specify the BES URL", required=False
+ )
+ parser.add_argument("-r", "--rest-url", help="Specify the REST URL", required=False)
+ parser.add_argument("-u", "--user", help="Specify the username", required=False)
+ parser.add_argument("-p", "--password", help="Specify the password", required=False)
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, config_ini, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ # set different log levels:
+ log_level = logging.INFO
+ if verbose:
+ log_level = logging.INFO
+ if verbose > 1:
+ log_level = logging.DEBUG
+
+ # get path to put log file in:
+ log_filename = os.path.join(invoke_folder, "serversettings.log")
+
+ print(f"Log File Path: {log_filename}")
+
+ handlers = [
+ logging.handlers.RotatingFileHandler(
+ log_filename, maxBytes=5 * 1024 * 1024, backupCount=1
+ )
+ ]
+
+ # log output to console if arg provided:
+ if verbose:
+ handlers.append(logging.StreamHandler())
+
+ # setup logging:
+ logging.basicConfig(
+ encoding="utf-8",
+ level=log_level,
+ format="%(asctime)s %(levelname)s:%(message)s",
+ handlers=handlers,
+ )
+ logging.info("----- Starting New Session ------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("Python version: %s", platform.sys.version)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("this plugin's version: %s", __version__)
+
+ password = args.password
+
+ if not password:
+ logging.warning("Password was not provided, provide REST API password.")
+ print("Password was not provided, provide REST API password.")
+ password = getpass.getpass()
+
+ # process args, setup connection:
+ rest_url = args.rest_url
+
+ # normalize url to https://HostOrIP:52311
+ if rest_url and rest_url.endswith("/api"):
+ rest_url = rest_url.replace("/api", "")
+
+ try:
+ bes_conn = besapi.besapi.BESConnection(args.user, password, rest_url)
+ # bes_conn.login()
+ except (
+ AttributeError,
+ ConnectionRefusedError,
+ besapi.besapi.requests.exceptions.ConnectionError,
+ ):
+ try:
+ # print(args.besserver)
+ bes_conn = besapi.besapi.BESConnection(args.user, password, args.besserver)
+ # handle case where args.besserver is None
+ # AttributeError: 'NoneType' object has no attribute 'startswith'
+ except AttributeError:
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+
+ # get config:
+ config_ini = get_config()
+
+ logging.info("getting settings_xml from config info")
+
+ # process settings
+ settings_xml = get_settings_xml(config_ini)
+
+ logging.debug(settings_xml)
+
+ rest_result = post_settings(settings_xml)
+
+ logging.info(rest_result)
+
+ logging.info("----- Ending Session ------")
+ print("main() End")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/session_relevance_from_file.py b/examples/session_relevance_from_file.py
new file mode 100644
index 0000000..78703e5
--- /dev/null
+++ b/examples/session_relevance_from_file.py
@@ -0,0 +1,121 @@
+"""
+Example session relevance results from a file
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import logging
+import ntpath
+import os
+import platform
+import sys
+
+import besapi
+import besapi.plugin_utilities
+
+__version__ = "1.2.1"
+verbose = 0
+invoke_folder = None
+
+
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ print("NOTE: this script requires besapi v3.3.3+ due to besapi.plugin_utilities")
+
+ parser = besapi.plugin_utilities.setup_plugin_argparse()
+
+ # add additonal arg specific to this script:
+ parser.add_argument(
+ "-f",
+ "--file",
+ help="text file to read session relevance query from",
+ required=False,
+ type=str,
+ default="examples/session_relevance_query_input.txt",
+ )
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ print(log_file_path)
+
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config(
+ log_file_path, verbose, args.console
+ )
+
+ logging.basicConfig(**logging_config)
+
+ logging.info("---------- Starting New Session -----------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("%s's version: %s", get_invoke_file_name(verbose), __version__)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("Python version: %s", platform.sys.version)
+
+ bes_conn = besapi.plugin_utilities.get_besapi_connection(args)
+
+ # args.file defaults to "examples/session_relevance_query_input.txt"
+ with open(args.file) as file:
+ session_relevance = file.read()
+
+ result = bes_conn.session_relevance_string(session_relevance)
+
+ logging.debug(result)
+
+ with open("examples/session_relevance_query_output.txt", "w") as file_out:
+ file_out.write(result)
+
+ logging.info("---------- END -----------")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/session_relevance_from_file_json.py b/examples/session_relevance_from_file_json.py
new file mode 100644
index 0000000..c0ddb78
--- /dev/null
+++ b/examples/session_relevance_from_file_json.py
@@ -0,0 +1,44 @@
+"""
+Example session relevance results in json format
+
+This is much more fragile because it uses GET instead of POST
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import json
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ with open("examples/session_relevance_query_input.txt") as file:
+ session_relevance = file.read()
+
+ data = {"output": "json", "relevance": session_relevance}
+
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ if __debug__:
+ print(result)
+
+ json_result = json.loads(str(result))
+
+ json_string = json.dumps(json_result, indent=2)
+
+ if __debug__:
+ print(json_string)
+
+ with open(
+ "examples/session_relevance_query_from_file_output.json", "w"
+ ) as file_out:
+ file_out.write(json_string)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/session_relevance_from_string.py b/examples/session_relevance_from_string.py
new file mode 100644
index 0000000..893326e
--- /dev/null
+++ b/examples/session_relevance_from_string.py
@@ -0,0 +1,37 @@
+"""
+Example session relevance results from a string
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import json
+
+import besapi
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ session_relevance = '(multiplicity of it, it) of unique values of (it as trimmed string) of (preceding text of first "|" of it | it) of values of results of bes properties "Installed Applications - Windows"'
+
+ data = {"output": "json", "relevance": session_relevance}
+
+ result = bes_conn.post(bes_conn.url("query"), data)
+
+ json_result = json.loads(str(result))
+
+ json_string = json.dumps(json_result, indent=2)
+
+ print(json_string)
+
+ with open(
+ "examples/session_relevance_query_from_string_output.json", "w"
+ ) as file_out:
+ file_out.write(json_string)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/session_relevance_query_input.txt b/examples/session_relevance_query_input.txt
new file mode 100644
index 0000000..c28d95b
--- /dev/null
+++ b/examples/session_relevance_query_input.txt
@@ -0,0 +1 @@
+(it as string) of (name of it, id of it) of members of items 1 of (it, bes computer groups) whose(item 0 of it = id of item 1 of it) of minimum of ids of bes computer groups whose (exists members of it)
diff --git a/examples/setup_server_plugin_service.py b/examples/setup_server_plugin_service.py
new file mode 100644
index 0000000..0b33543
--- /dev/null
+++ b/examples/setup_server_plugin_service.py
@@ -0,0 +1,262 @@
+"""
+Setup the root server server plugin service with creds provided
+
+requires `besapi`, install with command `pip install besapi`
+
+Example Usage:
+python setup_server_plugin_service.py -r https://localhost:52311/api -u API_USER -p API_PASSWORD
+
+References:
+- https://developer.bigfix.com/rest-api/api/admin.html
+- https://github.com/jgstew/besapi/blob/master/examples/rest_cmd_args.py
+- https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+"""
+
+import argparse
+import getpass
+import logging
+import logging.handlers
+import os
+import platform
+import sys
+
+import besapi
+
+__version__ = "0.0.1"
+verbose = 0
+bes_conn = None
+invoke_folder = None
+
+
+def get_invoke_folder():
+ """Get the folder the script was invoked from
+
+ References:
+ - https://github.com/jgstew/tools/blob/master/Python/locate_self.py
+ """
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+def test_file_exists(path):
+ """return true if file exists"""
+
+ if not (os.path.isfile(path) and os.access(path, os.R_OK)):
+ path = os.path.join(invoke_folder, path)
+
+ logging.info("testing if exists: `%s`", path)
+
+ if os.path.isfile(path) and os.access(path, os.R_OK) and os.access(path, os.W_OK):
+ return path
+
+ return False
+
+
+def main():
+ """Execution starts here"""
+ print("main() start")
+
+ parser = argparse.ArgumentParser(
+ description="Provde command line arguments for REST URL, username, and password"
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ help="Set verbose output",
+ required=False,
+ action="count",
+ default=0,
+ )
+ parser.add_argument(
+ "-besserver", "--besserver", help="Specify the BES URL", required=False
+ )
+ parser.add_argument("-r", "--rest-url", help="Specify the REST URL", required=False)
+ parser.add_argument("-u", "--user", help="Specify the username", required=False)
+ parser.add_argument("-p", "--password", help="Specify the password", required=False)
+ # allow unknown args to be parsed instead of throwing an error:
+ args, _unknown = parser.parse_known_args()
+
+ # allow set global scoped vars
+ global bes_conn, verbose, invoke_folder
+ verbose = args.verbose
+
+ # get folder the script was invoked from:
+ invoke_folder = get_invoke_folder()
+
+ # set different log levels:
+ log_level = logging.INFO
+ if verbose:
+ log_level = logging.INFO
+ if verbose > 1:
+ log_level = logging.DEBUG
+
+ # get path to put log file in:
+ log_filename = os.path.join(invoke_folder, "setup_server_plugin_service.log")
+
+ print(f"Log File Path: {log_filename}")
+
+ handlers = [
+ logging.handlers.RotatingFileHandler(
+ log_filename, maxBytes=5 * 1024 * 1024, backupCount=1
+ )
+ ]
+
+ # log output to console:
+ handlers.append(logging.StreamHandler())
+
+ # setup logging:
+ logging.basicConfig(
+ encoding="utf-8",
+ level=log_level,
+ format="%(asctime)s %(levelname)s:%(message)s",
+ handlers=handlers,
+ )
+ logging.info("----- Starting New Session ------")
+ logging.debug("invoke folder: %s", invoke_folder)
+ logging.debug("Python version: %s", platform.sys.version)
+ logging.debug("BESAPI Module version: %s", besapi.besapi.__version__)
+ logging.debug("this plugin's version: %s", __version__)
+
+ password = args.password
+
+ if not password:
+ logging.warning("Password was not provided, provide REST API password.")
+ print("Password was not provided, provide REST API password.")
+ password = getpass.getpass()
+
+ # process args, setup connection:
+ rest_url = args.rest_url
+
+ # normalize url to https://HostOrIP:52311
+ if rest_url and rest_url.endswith("/api"):
+ rest_url = rest_url.replace("/api", "")
+
+ try:
+ bes_conn = besapi.besapi.BESConnection(args.user, password, rest_url)
+ # bes_conn.login()
+ except (
+ AttributeError,
+ ConnectionRefusedError,
+ besapi.besapi.requests.exceptions.ConnectionError,
+ ):
+ try:
+ # print(args.besserver)
+ bes_conn = besapi.besapi.BESConnection(args.user, password, args.besserver)
+ # handle case where args.besserver is None
+ # AttributeError: 'NoneType' object has no attribute 'startswith'
+ except AttributeError:
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+
+ root_id = int(
+ bes_conn.session_relevance_string(
+ "unique value of ids of bes computers whose(root server flag of it)"
+ )
+ )
+
+ logging.info("Root server computer id: %s", root_id)
+
+ InstallPluginService_id = int(
+ bes_conn.session_relevance_string(
+ 'unique value of ids of fixlets whose(name of it contains "Install BES Server Plugin Service") of bes sites whose(name of it = "BES Support")'
+ )
+ )
+
+ logging.info(
+ "Install BES Server Plugin Service content id: %s", InstallPluginService_id
+ )
+
+ ConfigureCredentials_id = int(
+ bes_conn.session_relevance_string(
+ 'unique value of ids of fixlets whose(name of it contains "Configure REST API credentials for BES Server Plugin Service") of bes sites whose(name of it = "BES Support")'
+ )
+ )
+
+ logging.info(
+ "Configure REST API credentials for BES Server Plugin Service content id: %s",
+ ConfigureCredentials_id,
+ )
+
+ EnableWakeOnLAN_id = int(
+ bes_conn.session_relevance_string(
+ 'unique value of ids of fixlets whose(name of it contains "Enable Wake-on-LAN Medic") of bes sites whose(name of it = "BES Support")'
+ )
+ )
+
+ logging.info(
+ "Enable Wake-on-LAN Medic content id: %s",
+ EnableWakeOnLAN_id,
+ )
+
+ # Build the XML for the Multi Action Group to setup the plugin service:
+ XML_String_MultiActionGroup = f"""
+
+
+ Setup Server Plugin Service
+ exists main gather service
+
+ install initscripts
+
+
+
+ true
+
+
+
+ BES Support
+ {InstallPluginService_id}
+ Action1
+
+
+
+
+ BES Support
+ {ConfigureCredentials_id}
+ Action1
+
+ {args.user}
+
+
+
+
+
+ BES Support
+ {EnableWakeOnLAN_id}
+ Action1
+
+
+
+ true
+ P7D
+
+
+ {root_id}
+
+
+"""
+
+ # create action to setup server plugin service:
+ action_result = bes_conn.post("actions", XML_String_MultiActionGroup)
+
+ logging.info(action_result)
+
+ logging.info("----- Ending Session ------")
+ print("main() End")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/stop_open_completed_actions.py b/examples/stop_open_completed_actions.py
new file mode 100644
index 0000000..13f8dd7
--- /dev/null
+++ b/examples/stop_open_completed_actions.py
@@ -0,0 +1,27 @@
+import besapi
+
+# another session relevance option:
+# ids of bes actions whose( ("Expired" = state of it OR "Stopped" = state of it) AND (now - time issued of it > 180 * day) )
+
+SESSION_RELEVANCE = """ids of bes actions whose( (targeted by list flag of it OR targeted by id flag of it) AND not reapply flag of it AND not group member flag of it AND "Open"=state of it AND (now - time issued of it) >= 8 * day )"""
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ session_result = bes_conn.session_relevance_array(SESSION_RELEVANCE)
+
+ # print(session_result)
+
+ # https://developer.bigfix.com/rest-api/api/action.html
+ for action_id in session_result:
+ print("Stopping Action:", action_id)
+ action_stop_result = bes_conn.post("action/" + action_id + "/stop", "")
+ print(action_stop_result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/upload_files.py b/examples/upload_files.py
new file mode 100644
index 0000000..ea8efd6
--- /dev/null
+++ b/examples/upload_files.py
@@ -0,0 +1,33 @@
+"""
+Upload files in folder.
+
+requires `besapi`, install with command `pip install besapi`
+"""
+
+import os
+
+import besapi
+
+
+def main(path_folder="./tmp"):
+ """Execution starts here"""
+ print("main()")
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ print(f"INFO: Uploading new files within: {os.path.abspath(path_folder)}")
+
+ for entry in os.scandir(path_folder):
+ if entry.is_file() and "README.md" not in entry.path:
+ # this check for spaces is not required for besapi>=3.1.9
+ if " " in os.path.basename(entry.path):
+ print(f"ERROR: files cannot contain spaces! skipping: {entry.path}")
+ continue
+ print(f"Processing: {entry.path}")
+ output = bes_conn.upload(entry.path)
+ # print(output)
+ print(bes_conn.parse_upload_result_to_prefetch(output))
+
+
+if __name__ == "__main__":
+ main("./examples/upload_files")
diff --git a/examples/upload_files/README.md b/examples/upload_files/README.md
new file mode 100644
index 0000000..5ace3f2
--- /dev/null
+++ b/examples/upload_files/README.md
@@ -0,0 +1 @@
+put files in this folder to have them be uploaded to the root server by the script upload_files.py
diff --git a/examples/wake_on_lan.py b/examples/wake_on_lan.py
new file mode 100644
index 0000000..94d8e7a
--- /dev/null
+++ b/examples/wake_on_lan.py
@@ -0,0 +1,96 @@
+"""
+Send Wake On Lan (WoL) request to given computer IDs
+
+requires `besapi`, install with command `pip install besapi`
+
+Related:
+
+- https://support.hcltechsw.com/csm?id=kb_article&sysparm_article=KB0023378
+- http://localhost:__WebReportsPort__/json/wakeonlan?cid=_ComputerID_&cid=_NComputerID_
+- POST(binary) http://localhost:52311/data/wake-on-lan
+- https://localhost:52311/rd-proxy?RequestUrl=cgi-bin/bfenterprise/BESGatherMirrorNew.exe/-triggergatherdb?forwardtrigger
+- https://localhost:52311/rd-proxy?RequestUrl=../../cgi-bin/bfenterprise/ClientRegister.exe?RequestType=GetComputerID
+- https://localhost:52311/rd-proxy?RequestUrl=cgi-bin/bfenterprise/BESGatherMirror.exe/-besgather&body=SiteContents&url=http://_MASTHEAD_FQDN_:52311/cgi-bin/bfgather.exe/actionsite
+- https://localhost:52311/rd-proxy?RequestUrl=cgi-bin/bfenterprise/BESMirrorRequest.exe/-textreport
+- Gather Download Request: https://localhost:52311/rd-proxy?RequestUrl=bfmirror/downloads/_ACTION_ID_/_DOWNLOAD_ID_
+"""
+
+import besapi
+
+SESSION_RELEVANCE_COMPUTER_IDS = """
+ ids of bes computers
+ whose(root server flag of it AND now - last report time of it < 10 * day)
+"""
+
+
+def main():
+ """Execution starts here"""
+ print("main()")
+
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ bes_conn.login()
+
+ # SessionRelevance for computer ids you wish to wake:
+ # this currently returns the root server itself, which should have no real effect.
+ # change this to a singular or plural result of computer ids you wish to wake.
+ session_relevance = SESSION_RELEVANCE_COMPUTER_IDS
+
+ computer_id_array = bes_conn.session_relevance_array(session_relevance)
+
+ # print(computer_id_array)
+
+ computer_ids_xml_string = ""
+
+ for item in computer_id_array:
+ computer_ids_xml_string += ''
+
+ # print(computer_ids_xml_string)
+
+ soap_xml = (
+ """
+
+
+
+
+
+ """
+ + computer_ids_xml_string
+ + """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
+ )
+
+ result = bes_conn.session.post(
+ f"{bes_conn.rootserver}/WakeOnLan", data=soap_xml, verify=False
+ )
+
+ print(result)
+ print(result.text)
+
+ print("Finished, Response 200 should mean succces.")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/requirements.txt b/requirements.txt
index e871578..7f328c0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
cmd2
lxml
requests
+setuptools
diff --git a/setup.cfg b/setup.cfg
index 4a4fecf..fcc2420 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,7 +1,7 @@
[metadata]
# single source version in besapi.__init__.__version__
# can get version on command line with: `python setup.py --version`
-version = attr: besapi.__version__
+version = attr: besapi.besapi.__version__
long_description = file: README.md
long_description_content_type = text/markdown
classifiers =
@@ -10,4 +10,4 @@ classifiers =
License :: OSI Approved :: MIT License
[options]
-python_requires = >=3.6
+python_requires = >=3.7
diff --git a/setup.py b/setup.py
index 31a5a44..cafd69b 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@
description="Library for working with the BigFix REST API",
license="MIT",
keywords="bigfix iem tem rest api",
- url="https://github.com/CLCMacTeam/besapi",
+ url="https://github.com/jgstew/besapi",
# long_description= moved to setup.cfg
packages=["besapi", "bescli"],
package_data={"besapi": ["schemas/*.xsd"]},
diff --git a/src/besapi/__init__.py b/src/besapi/__init__.py
index 28f4ec5..920c7b5 100644
--- a/src/besapi/__init__.py
+++ b/src/besapi/__init__.py
@@ -5,5 +5,3 @@
# https://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path/4397291
from . import besapi
-
-__version__ = "3.0.2"
diff --git a/src/besapi/__main__.py b/src/besapi/__main__.py
index 6269c0c..f1aee0d 100644
--- a/src/besapi/__main__.py
+++ b/src/besapi/__main__.py
@@ -1,6 +1,10 @@
"""
To run this module directly
"""
+
+import logging
+
from bescli import bescli
+logging.basicConfig()
bescli.main()
diff --git a/src/besapi/besapi.py b/src/besapi/besapi.py
index 15511cc..b218cca 100644
--- a/src/besapi/besapi.py
+++ b/src/besapi/besapi.py
@@ -9,7 +9,10 @@
Library for communicating with the BES (BigFix) REST API.
"""
+import configparser
import datetime
+import hashlib
+import io
import json
import logging
import os
@@ -17,8 +20,6 @@
import site
import string
-# import urllib3.poolmanager
-
try:
from urllib import parse
except ImportError:
@@ -28,7 +29,8 @@
from lxml import etree, objectify
from pkg_resources import resource_filename
-logging.basicConfig(level=logging.WARNING)
+__version__ = "3.7.8"
+
besapi_logger = logging.getLogger("besapi")
@@ -78,7 +80,6 @@ def elem2dict(node):
else:
value = elem2dict(element)
if key in result:
-
if type(result[key]) is list:
result[key].append(value)
else:
@@ -113,12 +114,13 @@ def parse_bes_modtime(string_datetime):
return datetime.datetime.strptime(string_datetime, "%a, %d %b %Y %H:%M:%S %z")
+# import urllib3.poolmanager
# # https://docs.python-requests.org/en/latest/user/advanced/#transport-adapters
# class HTTPAdapterBiggerBlocksize(requests.adapters.HTTPAdapter):
# """custom HTTPAdapter for requests to override blocksize
# for Uploading or Downloading large files"""
-# # override inti_poolmanager from regular HTTPAdapter
+# # override init_poolmanager from regular HTTPAdapter
# # https://stackoverflow.com/questions/22915295/python-requests-post-and-big-content/22915488#comment125583017_22915488
# def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):
# """Initializes a urllib3 PoolManager.
@@ -150,11 +152,76 @@ def parse_bes_modtime(string_datetime):
# )
+def validate_xsd(doc):
+ """validate results using XML XSDs"""
+ try:
+ xmldoc = etree.fromstring(doc)
+ except BaseException: # pylint: disable=broad-except
+ return False
+
+ for xsd in ["BES.xsd", "BESAPI.xsd", "BESActionSettings.xsd"]:
+ xmlschema_doc = etree.parse(resource_filename(__name__, "schemas/%s" % xsd))
+
+ # one schema may throw an error while another will validate
+ try:
+ xmlschema = etree.XMLSchema(xmlschema_doc)
+ except etree.XMLSchemaParseError as err:
+ # this should only error if the XSD itself is malformed
+ besapi_logger.error("ERROR with `%s`: %s", xsd, err)
+ raise err
+
+ if xmlschema.validate(xmldoc):
+ return True
+
+ return False
+
+
+def get_bes_conn_using_config_file(conf_file=None):
+ """
+ read connection values from config file
+ return besapi connection
+ """
+ config_paths = [
+ "/etc/besapi.conf",
+ os.path.expanduser("~/besapi.conf"),
+ os.path.expanduser("~/.besapi.conf"),
+ "besapi.conf",
+ ]
+ # if conf_file specified, then only use that:
+ if conf_file:
+ config_paths = [conf_file]
+
+ configparser_instance = configparser.ConfigParser()
+
+ found_config_files = configparser_instance.read(config_paths)
+
+ if found_config_files and configparser_instance:
+ print("Attempting BESAPI Connection using config file:", found_config_files)
+ try:
+ BES_ROOT_SERVER = configparser_instance.get("besapi", "BES_ROOT_SERVER")
+ except BaseException: # pylint: disable=broad-except
+ BES_ROOT_SERVER = None
+
+ try:
+ BES_USER_NAME = configparser_instance.get("besapi", "BES_USER_NAME")
+ except BaseException: # pylint: disable=broad-except
+ BES_USER_NAME = None
+
+ try:
+ BES_PASSWORD = configparser_instance.get("besapi", "BES_PASSWORD")
+ except BaseException: # pylint: disable=broad-except
+ BES_PASSWORD = None
+
+ if BES_ROOT_SERVER and BES_USER_NAME and BES_PASSWORD:
+ return BESConnection(BES_USER_NAME, BES_PASSWORD, BES_ROOT_SERVER)
+
+ return None
+
+
class BESConnection:
"""BigFix RESTAPI connection abstraction class"""
def __init__(self, username, password, rootserver, verify=False):
-
if not verify:
# disable SSL warnings
requests.packages.urllib3.disable_warnings() # pylint: disable=no-member
@@ -181,7 +248,7 @@ def __init__(self, username, password, rootserver, verify=False):
try:
# get root server port
self.rootserver_port = int(rootserver.split("://", 1)[1].split(":", 1)[1])
- except BaseException:
+ except BaseException: # pylint: disable=broad-except
# if error, assume default
self.rootserver_port = 52311
@@ -272,20 +339,25 @@ def session_relevance_array(self, relevance, **kwargs):
if "no such child: Answer" in str(err):
try:
result.append("ERROR: " + rel_result.besobj.Query.Error.text)
- except AttributeError as err:
- if "no such child: Error" in str(err):
+ except AttributeError as err2:
+ if "no such child: Error" in str(err2):
result.append(" Nothing returned, but no error.")
besapi_logger.info("Query did not return any results")
else:
- besapi_logger.error("%s\n%s", err, rel_result.text)
+ besapi_logger.error("%s\n%s", err2, rel_result.text)
+ result.append("ERROR: " + rel_result.text)
raise
else:
+ besapi_logger.error("%s\n%s", err, rel_result.text)
+ result.append("ERROR: " + rel_result.text)
raise
return result
def session_relevance_string(self, relevance, **kwargs):
"""Get Session Relevance Results string"""
- rel_result_array = self.session_relevance_array(relevance, **kwargs)
+ rel_result_array = self.session_relevance_array(
+ "(it as string) of ( " + relevance + " )", **kwargs
+ )
return "\n".join(rel_result_array)
def login(self):
@@ -324,6 +396,34 @@ def logout(self):
self.session.cookies.clear()
self.session.close()
+ def set_dashboard_variable_value(
+ self, dashboard_name, var_name, var_value, private=False
+ ):
+ """set the variable value from a dashboard datastore"""
+
+ dash_var_xml = f"""
+
+ {dashboard_name}
+ {var_name}
+ {str(private).lower()}
+ {var_value}
+
+
+ """
+
+ return self.post(
+ f"dashboardvariable/{dashboard_name}/{var_name}", data=dash_var_xml
+ )
+
+ def get_dashboard_variable_value(self, dashboard_name, var_name):
+ """get the variable value from a dashboard datastore"""
+
+ return str(
+ self.get(
+ f"dashboardvariable/{dashboard_name}/{var_name}"
+ ).besobj.DashboardData.Value
+ )
+
def validate_site_path(self, site_path, check_site_exists=True, raise_error=False):
"""make sure site_path is valid"""
@@ -349,18 +449,18 @@ def validate_site_path(self, site_path, check_site_exists=True, raise_error=Fals
if not check_site_exists:
# don't check if site exists first
return site_path
- else:
- # check site exists first
- site_result = self.get(f"site/{site_path}")
- if site_result.request.status_code != 200:
- besapi_logger.info("Site `%s` does not exist", site_path)
- if not raise_error:
- return None
- raise ValueError(f"Site at path `{site_path}` does not exist!")
+ # check site exists first
+ site_result = self.get(f"site/{site_path}")
+ if site_result.request.status_code != 200:
+ besapi_logger.info("Site `%s` does not exist", site_path)
+ if not raise_error:
+ return None
- # site_path is valid and exists:
- return site_path
+ raise ValueError(f"Site at path `{site_path}` does not exist!")
+
+ # site_path is valid and exists:
+ return site_path
# Invalid: No valid prefix found
raise ValueError(
@@ -389,6 +489,29 @@ def set_current_site_path(self, site_path):
self.site_path = site_path
return self.site_path
+ def import_bes_to_site(self, bes_file_path, site_path=None):
+ """import bes file to site"""
+
+ if not os.access(bes_file_path, os.R_OK):
+ besapi_logger.error("%s is not readable", bes_file_path)
+ raise FileNotFoundError(f"{bes_file_path} is not readable")
+
+ site_path = self.get_current_site_path(site_path)
+
+ self.validate_site_path(site_path, False, True)
+
+ with open(bes_file_path, "rb") as f:
+ content = f.read()
+
+ # validate BES File contents:
+ if not validate_xsd(content):
+ besapi_logger.error("%s is not valid", bes_file_path)
+ return None
+
+ # https://developer.bigfix.com/rest-api/api/import.html
+ result = self.post(f"import/{site_path}", content)
+ return result
+
def create_site_from_file(self, bes_file_path, site_type="custom"):
"""create new site"""
xml_parsed = etree.parse(bes_file_path)
@@ -463,7 +586,33 @@ def create_group_from_file(self, bes_file_path, site_path=None):
return self.get_computergroup(site_path, new_group_name)
- def upload(self, file_path, file_name=None):
+ def get_upload(self, file_name, file_hash):
+ """
+ check for a specific file upload reference
+
+ each upload is uniquely identified by sha1 and filename
+
+ - https://developer.bigfix.com/rest-api/api/upload.html
+ - https://github.com/jgstew/besapi/issues/3
+ """
+ if len(file_hash) != 40:
+ raise ValueError("Invalid SHA1 Hash! Must be 40 characters!")
+
+ if " " in file_hash or " " in file_name:
+ raise ValueError("file name and hash cannot contain spaces")
+
+ if len(file_name) > 0:
+ result = self.get(self.url("upload/" + file_hash + "/" + file_name))
+ else:
+ raise ValueError("No file_name specified. Must be at least one character.")
+
+ if "Upload not found" in result.text:
+ # print("WARNING: Upload not found!")
+ return None
+
+ return result
+
+ def upload(self, file_path, file_name=None, file_hash=None):
"""
upload a single file
https://developer.bigfix.com/rest-api/api/upload.html
@@ -476,6 +625,38 @@ def upload(self, file_path, file_name=None):
if not file_name:
file_name = os.path.basename(file_path)
+ # files cannot contain spaces:
+ if " " in file_name:
+ besapi_logger.warning(
+ "Replacing spaces with underscores in `%s`", file_name
+ )
+ file_name = file_name.replace(" ", "_")
+
+ if not file_hash:
+ besapi_logger.warning(
+ "SHA1 hash of file to be uploaded not provided, calculating it."
+ )
+ sha1 = hashlib.sha1()
+ with open(file_path, "rb") as f:
+ while True:
+ # read 64k chunks
+ data = f.read(65536)
+ if not data:
+ break
+ sha1.update(data)
+ file_hash = sha1.hexdigest()
+
+ check_upload = None
+ if file_hash:
+ check_upload = self.get_upload(str(file_name), str(file_hash))
+
+ if check_upload:
+ besapi_logger.warning(
+ "Existing Matching Upload Found, Skipping Upload!"
+ )
+ # return same data as if we had uploaded
+ return check_upload
+
# Example Header:: Content-Disposition: attachment; filename="file.xml"
headers = {"Content-Disposition": f'attachment; filename="{file_name}"'}
with open(file_path, "rb") as f:
@@ -488,7 +669,7 @@ def parse_upload_result_to_prefetch(
file_url = str(result_upload.besobj.FileUpload.URL)
if use_https:
file_url = file_url.replace("http://", "https://")
- # there are 3 different posibilities for the server FQDN
+ # there are 3 different possibilities for the server FQDN
# localhost
# self.rootserver (without port number)
# the returned value from the upload result
@@ -526,6 +707,13 @@ def update_item_from_file(self, file_path, site_path=None):
"""update an item by name and last modified"""
site_path = self.get_current_site_path(site_path)
bes_tree = etree.parse(file_path)
+
+ with open(file_path, "rb") as f:
+ content = f.read()
+ if not validate_xsd(content):
+ besapi_logger.error("%s is not valid", file_path)
+ return None
+
# get name of first child tag of BES
# - https://stackoverflow.com/a/3601919/861745
bes_type = str(bes_tree.xpath("name(/BES/*[1])"))
@@ -739,7 +927,7 @@ def __init__(self, request):
f"\n - HTTP Response Status Code: `403` Forbidden\n - ERROR: `{self.text}`\n - URL: `{self.request.url}`"
)
- besapi_logger.info(
+ besapi_logger.debug(
"HTTP Request Status Code `%d` from URL `%s`",
self.request.status_code,
self.request.url,
@@ -768,7 +956,7 @@ def __str__(self):
# I think this is needed for python3 compatibility:
try:
return self.besxml.decode("utf-8")
- except BaseException:
+ except BaseException: # pylint: disable=broad-except
return self.besxml
else:
return self.text
@@ -813,26 +1001,7 @@ def besjson(self):
def validate_xsd(self, doc):
"""validate results using XML XSDs"""
- try:
- xmldoc = etree.fromstring(doc)
- except BaseException:
- return False
-
- for xsd in ["BES.xsd", "BESAPI.xsd", "BESActionSettings.xsd"]:
- xmlschema_doc = etree.parse(resource_filename(__name__, "schemas/%s" % xsd))
-
- # one schema may throw an error while another will validate
- try:
- xmlschema = etree.XMLSchema(xmlschema_doc)
- except etree.XMLSchemaParseError as err:
- # this should only error if the XSD itself is malformed
- besapi_logger.error("ERROR with `%s`: %s", xsd, err)
- raise err
-
- if xmlschema.validate(xmldoc):
- return True
-
- return False
+ return validate_xsd(doc)
def xmlparse_text(self, text):
"""parse response text as xml"""
@@ -865,4 +1034,5 @@ def main():
if __name__ == "__main__":
+ logging.basicConfig()
main()
diff --git a/src/besapi/plugin_utilities.py b/src/besapi/plugin_utilities.py
new file mode 100644
index 0000000..27303e8
--- /dev/null
+++ b/src/besapi/plugin_utilities.py
@@ -0,0 +1,197 @@
+"""This is a set of utility functions for use in multiple plugins
+
+see example here: https://github.com/jgstew/besapi/blob/master/examples/export_all_sites.py
+"""
+
+import argparse
+import getpass
+import logging
+import logging.handlers
+import ntpath
+import os
+import sys
+
+import besapi
+
+
+# NOTE: This does not work as expected when run from plugin_utilities
+def get_invoke_folder(verbose=0):
+ """Get the folder the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_folder = os.path.abspath(os.path.dirname(sys.executable))
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_folder = os.path.abspath(os.path.dirname(__file__))
+
+ if verbose:
+ print(f"invoke_folder = {invoke_folder}")
+
+ return invoke_folder
+
+
+# NOTE: This does not work as expected when run from plugin_utilities
+def get_invoke_file_name(verbose=0):
+ """Get the filename the script was invoked from"""
+ # using logging here won't actually log it to the file:
+
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ if verbose:
+ print("running in a PyInstaller bundle")
+ invoke_file_path = sys.executable
+ else:
+ if verbose:
+ print("running in a normal Python process")
+ invoke_file_path = __file__
+
+ if verbose:
+ print(f"invoke_file_path = {invoke_file_path}")
+
+ # get just the file name, return without file extension:
+ return os.path.splitext(ntpath.basename(invoke_file_path))[0]
+
+
+def setup_plugin_argparse(plugin_args_required=False):
+ """setup argparse for plugin use"""
+ arg_parser = argparse.ArgumentParser(
+ description="Provde command line arguments for REST URL, username, and password"
+ )
+ arg_parser.add_argument(
+ "-v",
+ "--verbose",
+ help="Set verbose output",
+ required=False,
+ action="count",
+ default=0,
+ )
+ arg_parser.add_argument(
+ "-c",
+ "--console",
+ help="log output to console",
+ required=False,
+ action="store_true",
+ )
+ arg_parser.add_argument(
+ "-besserver", "--besserver", help="Specify the BES URL", required=False
+ )
+ arg_parser.add_argument(
+ "-r", "--rest-url", help="Specify the REST URL", required=plugin_args_required
+ )
+ arg_parser.add_argument(
+ "-u", "--user", help="Specify the username", required=plugin_args_required
+ )
+ arg_parser.add_argument(
+ "-p", "--password", help="Specify the password", required=False
+ )
+
+ return arg_parser
+
+
+def get_plugin_logging_config(log_file_path="", verbose=0, console=True):
+ """get config for logging for plugin use
+
+ use this like: logging.basicConfig(**logging_config)"""
+
+ if not log_file_path or log_file_path == "":
+ log_file_path = os.path.join(
+ get_invoke_folder(verbose), get_invoke_file_name(verbose) + ".log"
+ )
+
+ # set different log levels:
+ log_level = logging.WARNING
+ if verbose:
+ log_level = logging.INFO
+ print("INFO: Log File Path:", log_file_path)
+ if verbose > 1:
+ log_level = logging.DEBUG
+
+ handlers = [
+ logging.handlers.RotatingFileHandler(
+ log_file_path, maxBytes=5 * 1024 * 1024, backupCount=1
+ )
+ ]
+
+ # log output to console if arg provided:
+ if console:
+ handlers.append(logging.StreamHandler())
+ print("INFO: also logging to console")
+
+ # return logging config:
+ return {
+ "encoding": "utf-8",
+ "level": log_level,
+ "format": "%(asctime)s %(levelname)s:%(message)s",
+ "handlers": handlers,
+ "force": True,
+ }
+
+
+def get_besapi_connection(args):
+ """get connection to besapi using either args or config file if args not provided"""
+
+ password = args.password
+
+ # if user was provided as arg but password was not:
+ if args.user and not password:
+ logging.warning("Password was not provided, provide REST API password.")
+ print("Password was not provided, provide REST API password:")
+ password = getpass.getpass()
+
+ if args.user:
+ logging.debug("REST API Password Length: %s", len(password))
+
+ # process args, setup connection:
+ rest_url = args.rest_url
+
+ # normalize url to https://HostOrIP:52311
+ if rest_url and rest_url.endswith("/api"):
+ rest_url = rest_url.replace("/api", "")
+
+ # attempt bigfix connection with provided args:
+ if args.user and password:
+ try:
+ if not rest_url:
+ raise AttributeError
+ bes_conn = besapi.besapi.BESConnection(args.user, password, rest_url)
+ except (
+ AttributeError,
+ ConnectionRefusedError,
+ besapi.besapi.requests.exceptions.ConnectionError,
+ ):
+ logging.exception(
+ "connection to `%s` failed, attempting `%s` instead",
+ rest_url,
+ args.besserver,
+ )
+ try:
+ if not args.besserver:
+ raise AttributeError
+ bes_conn = besapi.besapi.BESConnection(
+ args.user, password, args.besserver
+ )
+ # handle case where args.besserver is None
+ # AttributeError: 'NoneType' object has no attribute 'startswith'
+ except AttributeError:
+ logging.exception("----- ERROR: BigFix Connection Failed ------")
+ logging.exception(
+ "attempts to connect to BigFix using rest_url and besserver both failed"
+ )
+ return None
+ except BaseException as err:
+ # always log error and stop the current process
+ logging.exception("ERROR: %s", err)
+ logging.exception(
+ "----- ERROR: BigFix Connection Failed! Unknown reason ------"
+ )
+ return None
+ else:
+ logging.info(
+ "attempting connection to BigFix using config file method as user command arg was not provided"
+ )
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+
+ return bes_conn
diff --git a/src/bescli/__init__.py b/src/bescli/__init__.py
index b2069aa..c3e459b 100644
--- a/src/bescli/__init__.py
+++ b/src/bescli/__init__.py
@@ -1,6 +1,7 @@
"""
bescli provides a command line interface to interact with besapi
"""
+
# https://stackoverflow.com/questions/279237/import-a-module-from-a-relative-path/4397291
from . import bescli
diff --git a/src/bescli/__main__.py b/src/bescli/__main__.py
index 789509e..ff6901a 100644
--- a/src/bescli/__main__.py
+++ b/src/bescli/__main__.py
@@ -1,6 +1,10 @@
"""
To run this module directly
"""
+
+import logging
+
from . import bescli
+logging.basicConfig()
bescli.main()
diff --git a/src/bescli/bescli.py b/src/bescli/bescli.py
index 4e70558..d15a4f6 100644
--- a/src/bescli/bescli.py
+++ b/src/bescli/bescli.py
@@ -10,6 +10,7 @@
"""
import getpass
+import json
import logging
import os
import site
@@ -28,7 +29,10 @@
# this is for the case in which we are calling bescli from besapi
import besapi
-from besapi import __version__
+try:
+ from besapi.besapi import __version__
+except ImportError:
+ from besapi import __version__
class BESCLInterface(Cmd):
@@ -36,6 +40,13 @@ class BESCLInterface(Cmd):
def __init__(self, **kwargs):
Cmd.__init__(self, **kwargs)
+
+ # set an intro message
+ self.intro = (
+ f"\nWelcome to the BigFix REST API Interactive Python Module v{__version__}"
+ )
+
+ # sets the prompt look:
self.prompt = "BigFix> "
self.num_errors = 0
@@ -46,11 +57,56 @@ def __init__(self, **kwargs):
# set default config file path
self.conf_path = os.path.expanduser("~/.besapi.conf")
self.CONFPARSER = SafeConfigParser()
+ # for completion:
+ self.api_resources = []
self.do_conf()
+ def parse_help_resources(self):
+ """get api resources from help"""
+ if self.bes_conn:
+ help_result = self.bes_conn.get("help")
+ help_result = help_result.text.split("\n")
+ # print(help_result)
+ help_resources = []
+ for item in help_result:
+ if "/api/" in item:
+ _, _, res = item.partition("/api/")
+ # strip whitespace just in case:
+ help_resources.append(res.strip())
+
+ return help_resources
+ else:
+ return [
+ "actions",
+ "clientqueryresults",
+ "dashboardvariables",
+ "help",
+ "login",
+ "query",
+ "relaysites",
+ "serverinfo",
+ "sites",
+ ]
+
+ def complete_api_resources(self, text, line, begidx, endidx):
+ """define completion for apis"""
+
+ # only initialize once
+ if not self.api_resources:
+ self.api_resources = self.parse_help_resources()
+
+ # TODO: make this work to complete only the first word after get/post/delete
+ # return the matching subset:
+ return [name for name in self.api_resources if name.startswith(text)]
+
+ complete_get = complete_api_resources
+
def do_get(self, line):
"""Perform get request to BigFix server using provided api endpoint argument"""
+ # remove any extra whitespace
+ line = line.strip()
+
# Remove root server prefix:
# if root server prefix is not removed
# and root server is given as IP Address,
@@ -67,20 +123,44 @@ def do_get(self, line):
b = self.bes_conn.get(robjs[0])
# print objectify.ObjectPath(robjs[1:])
if b:
- print(eval("b()." + ".".join(robjs[1:])))
+ self.poutput(eval("b()." + ".".join(robjs[1:])))
else:
output_item = self.bes_conn.get(line)
- # print(type(output_item))
- print(output_item)
- # print(output_item.besdict)
- # print(output_item.besjson)
+ # self.poutput(type(output_item))
+ self.poutput(output_item)
+ # self.poutput(output_item.besdict)
+ # self.poutput(output_item.besjson)
+ else:
+ self.pfeedback("Not currently logged in. Type 'login'.")
+
+ complete_delete = complete_api_resources
+
+ def do_delete(self, line):
+ """Perform delete request to BigFix server using provided api endpoint argument"""
+
+ # remove any extra whitespace
+ line = line.strip()
+
+ # Remove root server prefix:
+ if "/api/" in line:
+ line = str(line).split("/api/", 1)[1]
+ self.pfeedback("get " + line)
+
+ if self.bes_conn:
+ output_item = self.bes_conn.delete(line)
+
+ self.poutput(output_item)
+ # self.poutput(output_item.besdict)
+ # self.poutput(output_item.besjson)
else:
self.pfeedback("Not currently logged in. Type 'login'.")
+ complete_post = complete_api_resources
+
def do_post(self, statement):
"""post file as data to path"""
- print(statement)
- print("not yet implemented")
+ self.poutput(statement)
+ self.poutput("not yet implemented")
def do_config(self, conf_file=None):
"""Attempt to load config info from file and login"""
@@ -111,7 +191,6 @@ def do_conf(self, conf_file=None):
self.conf_path = found_config_files[0]
if self.CONFPARSER:
-
try:
self.BES_ROOT_SERVER = self.CONFPARSER.get("besapi", "BES_ROOT_SERVER")
except BaseException:
@@ -212,7 +291,7 @@ def do_login(self, user=None):
else:
self.perror("Login Error!")
- def do_logout(self, arg=None):
+ def do_logout(self, _=None):
"""Logout and clear session"""
if self.bes_conn:
self.bes_conn.logout()
@@ -222,7 +301,7 @@ def do_logout(self, arg=None):
def do_debug(self, setting):
"""Enable or Disable Debug Mode"""
- print(bool(setting))
+ self.poutput(bool(setting))
self.debug = bool(setting)
self.echo = bool(setting)
self.quiet = bool(setting)
@@ -256,12 +335,12 @@ def do_saveconfig(self, arg=None):
"""save current config to file"""
self.do_saveconf(arg)
- def do_saveconf(self, arg=None):
+ def do_saveconf(self, _=None):
"""save current config to file"""
if not self.bes_conn:
self.do_login()
if not self.bes_conn:
- print("Can't save config without working login")
+ self.poutput("Can't save config without working login")
else:
conf_file_path = self.conf_path
self.pfeedback(f"Saving Config File to: {conf_file_path}")
@@ -272,29 +351,31 @@ def do_showconfig(self, arg=None):
"""List the current settings and connection status"""
self.do_ls(arg)
- def do_ls(self, arg=None):
+ def do_ls(self, _=None):
"""List the current settings and connection status"""
- print(" Connected: " + str(bool(self.bes_conn)))
- print(
+ self.poutput(" Connected: " + str(bool(self.bes_conn)))
+ self.poutput(
" BES_ROOT_SERVER: "
+ (self.BES_ROOT_SERVER if self.BES_ROOT_SERVER else "")
)
- print(
+ self.poutput(
" BES_USER_NAME: " + (self.BES_USER_NAME if self.BES_USER_NAME else "")
)
- print(
+ self.poutput(
" Password Length: "
+ str(len(self.BES_PASSWORD if self.BES_PASSWORD else ""))
)
- print(" Config File Path: " + self.conf_path)
+ self.poutput(" Config File Path: " + self.conf_path)
if self.bes_conn:
- print("Current Site Path: " + self.bes_conn.get_current_site_path(None))
+ self.poutput(
+ "Current Site Path: " + self.bes_conn.get_current_site_path(None)
+ )
- def do_error_count(self, arg=None):
+ def do_error_count(self, _=None):
"""Output the number of errors"""
self.poutput(f"Error Count: {self.num_errors}")
- def do_exit(self, arg=None):
+ def do_exit(self, _=None):
"""Exit this application"""
self.exit_code = self.num_errors
# no matter what I try I can't get anything but exit code 0 on windows
@@ -315,7 +396,7 @@ def do_query(self, statement):
self.pfeedback("A: ")
self.poutput(rel_result)
- def do_version(self, statement=None):
+ def do_version(self, _=None):
"""output version of besapi"""
self.poutput(f"besapi version: {__version__}")
@@ -329,7 +410,7 @@ def do_get_operator(self, statement=None):
result_op = self.bes_conn.get_user(statement)
self.poutput(result_op)
- def do_get_current_site(self, statement=None):
+ def do_get_current_site(self, _=None):
"""output current site path context"""
self.poutput(
f"Current Site Path: `{ self.bes_conn.get_current_site_path(None) }`"
@@ -343,11 +424,11 @@ def do_set_current_site(self, statement=None):
def do_get_content(self, resource_url):
"""get a specific item by resource url"""
- print(self.bes_conn.get_content_by_resource(resource_url))
+ self.poutput(self.bes_conn.get_content_by_resource(resource_url))
def do_export_item_by_resource(self, statement):
"""export content itemb to current folder"""
- print(self.bes_conn.export_item_by_resource(statement))
+ self.poutput(self.bes_conn.export_item_by_resource(statement))
def do_export_site(self, site_path):
"""export site contents to current folder"""
@@ -355,56 +436,80 @@ def do_export_site(self, site_path):
site_path, verbose=True, include_site_folder=False, include_item_ids=False
)
- def do_export_all_sites(self, statement=None):
+ def do_export_all_sites(self, _=None):
"""export site contents to current folder"""
self.bes_conn.export_all_sites(verbose=False)
+ complete_import_bes = Cmd.path_complete
+
+ def do_import_bes(self, statement):
+ """import bes file"""
+
+ bes_file_path = str(statement.args).strip()
+
+ site_path = self.bes_conn.get_current_site_path(None)
+
+ self.poutput(f"Import file: {bes_file_path}")
+
+ self.poutput(self.bes_conn.import_bes_to_site(bes_file_path, site_path))
+
complete_upload = Cmd.path_complete
def do_upload(self, file_path):
"""upload file to root server"""
if not os.access(file_path, os.R_OK):
- print(file_path, "is not a readable file")
+ self.poutput(file_path, "is not a readable file")
else:
upload_result = self.bes_conn.upload(file_path)
- print(upload_result)
- print(self.bes_conn.parse_upload_result_to_prefetch(upload_result))
+ self.poutput(upload_result)
+ self.poutput(self.bes_conn.parse_upload_result_to_prefetch(upload_result))
complete_create_group = Cmd.path_complete
def do_create_group(self, file_path):
"""create bigfix group from bes file"""
if not os.access(file_path, os.R_OK):
- print(file_path, "is not a readable file")
+ self.poutput(file_path, "is not a readable file")
else:
- print(self.bes_conn.create_group_from_file(file_path))
+ self.poutput(self.bes_conn.create_group_from_file(file_path))
complete_create_user = Cmd.path_complete
def do_create_user(self, file_path):
"""create bigfix user from bes file"""
if not os.access(file_path, os.R_OK):
- print(file_path, "is not a readable file")
+ self.poutput(file_path, "is not a readable file")
else:
- print(self.bes_conn.create_user_from_file(file_path))
+ self.poutput(self.bes_conn.create_user_from_file(file_path))
complete_create_site = Cmd.path_complete
def do_create_site(self, file_path):
"""create bigfix site from bes file"""
if not os.access(file_path, os.R_OK):
- print(file_path, "is not a readable file")
+ self.poutput(file_path, "is not a readable file")
else:
- print(self.bes_conn.create_site_from_file(file_path))
+ self.poutput(self.bes_conn.create_site_from_file(file_path))
complete_update_item = Cmd.path_complete
def do_update_item(self, file_path):
"""update bigfix content item from bes file"""
if not os.access(file_path, os.R_OK):
- print(file_path, "is not a readable file")
+ self.poutput(file_path, "is not a readable file")
else:
- print(self.bes_conn.update_item_from_file(file_path))
+ self.poutput(self.bes_conn.update_item_from_file(file_path))
+
+ def do_serverinfo(self, _=None):
+ """get server info and return formatted"""
+
+ # not sure what the minimum version for this is:
+ result = self.bes_conn.get("serverinfo")
+
+ result_json = json.loads(result.text)
+
+ self.poutput(f"\nServer Info for {self.BES_ROOT_SERVER}")
+ self.poutput(json.dumps(result_json, indent=2))
def main():
@@ -413,4 +518,5 @@ def main():
if __name__ == "__main__":
+ logging.basicConfig()
main()
diff --git a/tests/tests.py b/tests/tests.py
index b4d3f21..6b156c9 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -5,6 +5,7 @@
import argparse
import os
+import random
import subprocess
import sys
@@ -24,8 +25,9 @@
sys.path.reverse()
import besapi
+import besapi.plugin_utilities
-print("besapi version: " + str(besapi.__version__))
+print("besapi version: " + str(besapi.besapi.__version__))
assert 15 == len(besapi.besapi.rand_password(15))
@@ -105,7 +107,9 @@ class RequestResult(object):
# this should really only run if the config file is present:
if bigfix_cli.bes_conn:
# session relevance tests require functioning web reports server
- print(bigfix_cli.bes_conn.session_relevance_string("number of bes computers"))
+ assert (
+ int(bigfix_cli.bes_conn.session_relevance_string("number of bes computers")) > 0
+ )
assert (
"test session relevance string result"
in bigfix_cli.bes_conn.session_relevance_string(
@@ -124,11 +128,51 @@ class RequestResult(object):
upload_result = bigfix_cli.bes_conn.upload(
"./besapi/__init__.py", "test_besapi_upload.txt"
)
- print(upload_result)
+ # print(upload_result)
+ assert "test_besapi_upload.txt" in str(upload_result)
print(bigfix_cli.bes_conn.parse_upload_result_to_prefetch(upload_result))
+ dashboard_name = "_PyBESAPI_tests.py"
+ var_name = "TestVarName"
+ var_value = "TestVarValue " + str(random.randint(0, 9999))
+
+ assert var_value in str(
+ bigfix_cli.bes_conn.set_dashboard_variable_value(
+ dashboard_name, var_name, var_value
+ )
+ )
+
+ assert var_value in str(
+ bigfix_cli.bes_conn.get_dashboard_variable_value(dashboard_name, var_name)
+ )
+
if os.name == "nt":
subprocess.run(
'CMD /C python -m besapi ls clear ls conf "query number of bes computers" version error_count exit',
check=True,
)
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
+ print("login succeeded:", bes_conn.login())
+
+# test plugin_utilities:
+print(besapi.plugin_utilities.get_invoke_folder())
+print(besapi.plugin_utilities.get_invoke_file_name())
+
+parser = besapi.plugin_utilities.setup_plugin_argparse(plugin_args_required=False)
+# allow unknown args to be parsed instead of throwing an error:
+args, _unknown = parser.parse_known_args()
+
+# test logging plugin_utilities:
+import logging
+
+logging_config = besapi.plugin_utilities.get_plugin_logging_config("./tests.log")
+
+# this use of logging.basicConfig requires python >= 3.9
+if sys.version_info >= (3, 9):
+ logging.basicConfig(**logging_config)
+
+ logging.warning("Just testing to see if logging is working!")
+
+ assert os.path.isfile("./tests.log")
+
+sys.exit(0)