Add composer-cli utility and implement the recipes commands
composer-cli --help shows the commands. Output defaults to human readable, but raw json can be displayed by passing --json
This commit is contained in:
parent
79fa1c957e
commit
d2f784e5da
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
*.pyc
|
||||
src/pylorax/version.py*
|
||||
src/composer/version.py*
|
||||
*.swp
|
||||
.pylint.d/
|
||||
_build/
|
||||
|
6
Makefile
6
Makefile
@ -11,10 +11,13 @@ USER_SITE_PACKAGES ?= $(shell sudo $(PYTHON) -m site --user-site)
|
||||
|
||||
default: all
|
||||
|
||||
src/composer/version.py: lorax.spec
|
||||
echo "num = '$(VERSION)-$(RELEASE)'" > src/composer/version.py
|
||||
|
||||
src/pylorax/version.py: lorax.spec
|
||||
echo "num = '$(VERSION)-$(RELEASE)'" > src/pylorax/version.py
|
||||
|
||||
all: src/pylorax/version.py
|
||||
all: src/pylorax/version.py src/composer/version.py
|
||||
$(PYTHON) setup.py build
|
||||
|
||||
install: all
|
||||
@ -42,6 +45,7 @@ test: docs
|
||||
|
||||
clean:
|
||||
-rm -rf build src/pylorax/version.py
|
||||
-rm -rf build src/composer/version.py
|
||||
|
||||
tag:
|
||||
git tag -f $(TAG)
|
||||
|
13
lorax.spec
13
lorax.spec
@ -100,6 +100,16 @@ BuildRequires: systemd
|
||||
%description composer
|
||||
lorax-composer provides a REST API for building images using lorax.
|
||||
|
||||
%package -n composer-cli
|
||||
Summary: A command line tool for use with the lorax-composer API server
|
||||
|
||||
# From Distribution
|
||||
Requires: python-urllib3
|
||||
|
||||
%description -n composer-cli
|
||||
A command line tool for use with the lorax-composer API server. Examine recipes,
|
||||
build images, etc. from the command line.
|
||||
|
||||
%prep
|
||||
%setup -q
|
||||
|
||||
@ -148,6 +158,9 @@ getent passwd weldr >/dev/null 2>&1 || useradd -r -g weldr -d / -s /sbin/nologin
|
||||
%{_sbindir}/lorax-composer
|
||||
%{_unitdir}/lorax-composer.service
|
||||
|
||||
%files -n composer-cli
|
||||
%{_bindir}/composer-cli
|
||||
|
||||
%changelog
|
||||
* Thu Feb 22 2018 Brian C. Lane <bcl@redhat.com> 19.7.10-1
|
||||
- Add the partitioned-disk.ks file for the new output type (bcl)
|
||||
|
3
setup.py
3
setup.py
@ -20,7 +20,8 @@ for root, dnames, fnames in os.walk("share"):
|
||||
data_files.append(("/usr/sbin", ["src/sbin/lorax", "src/sbin/mkefiboot",
|
||||
"src/sbin/livemedia-creator", "src/sbin/lorax-composer"]))
|
||||
data_files.append(("/usr/bin", ["src/bin/image-minimizer",
|
||||
"src/bin/mk-s390-cdboot"]))
|
||||
"src/bin/mk-s390-cdboot",
|
||||
"src/bin/composer-cli"]))
|
||||
|
||||
# get the version
|
||||
sys.path.insert(0, "src")
|
||||
|
124
src/bin/composer-cli
Executable file
124
src/bin/composer-cli
Executable file
@ -0,0 +1,124 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# composer-cli
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
log = logging.getLogger("composer-cli")
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from composer import vernum
|
||||
from composer.cli import main
|
||||
|
||||
VERSION = "{0}-{1}".format(os.path.basename(sys.argv[0]), vernum)
|
||||
|
||||
# Documentation for the commands
|
||||
epilog = """
|
||||
compose tar <recipe> Depsolve Recipe and compose a tar file using export from bdcs
|
||||
recipes list List the names of the available recipes.
|
||||
show <recipe,...> Display the recipe in TOML format.
|
||||
changes <recipe,...> Display the changes for each recipe.
|
||||
diff <recipe-name> Display the differences between 2 versions of a recipe.
|
||||
<from-commit> Commit hash or NEWEST
|
||||
<to-commit> Commit hash, NEWEST, or WORKSPACE
|
||||
save <recipe,...> Save the recipe to a file, <recipe-name>.toml
|
||||
delete <recipe> Delete a recipe from the server
|
||||
depsolve <recipe,...> Display the packages needed to install the recipe.
|
||||
push <recipe> Push a recipe TOML file to the server.
|
||||
freeze <recipe,...> Display the frozen recipe's modules and packages.
|
||||
freeze show <recipe,...> Display the frozen recipe in TOML format.
|
||||
freeze save <recipe,...> Save the frozen recipe to a file, <recipe-name>.frozen.toml.
|
||||
tag <recipe> Tag the most recent recipe commit as a release.
|
||||
undo <recipe> <commit> Undo changes to a recipe by reverting to the selected commit.
|
||||
workspace <recipe> Push the recipe TOML to the temporary workspace storage.
|
||||
modules list List the available modules.
|
||||
projects list List the available projects.
|
||||
"""
|
||||
|
||||
def get_parser():
|
||||
""" Return the ArgumentParser for composer-cli"""
|
||||
|
||||
parser = argparse.ArgumentParser(description="Lorax Composer commandline tool",
|
||||
epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
fromfile_prefix_chars="@")
|
||||
|
||||
parser.add_argument("-j", "--json", action="store_true", default=False,
|
||||
help="Output the raw JSON response instead of the normal output.")
|
||||
parser.add_argument("-s", "--socket", default="/run/weldr/api.socket", metavar="SOCKET",
|
||||
help="Path to the socket file to listen on")
|
||||
parser.add_argument("--log", dest="logfile", default="/var/log/lorax-composer/cli.log", metavar="LOG",
|
||||
help="Path to logfile (/var/log/lorax-composer/cli.log)")
|
||||
parser.add_argument("-a", "--api", dest="api_version", default="0", metavar="APIVER",
|
||||
help="API Version to use")
|
||||
parser.add_argument("-V", action="store_true", dest="showver",
|
||||
help="show program's version number and exit")
|
||||
|
||||
# Commands are implemented by parsing the remaining arguments outside of argparse
|
||||
parser.add_argument('args', nargs=argparse.REMAINDER)
|
||||
|
||||
|
||||
return parser
|
||||
|
||||
def setup_logging(logfile):
|
||||
# Setup logging to console and to logfile
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
sh = logging.StreamHandler()
|
||||
sh.setLevel(logging.INFO)
|
||||
fmt = logging.Formatter("%(asctime)s: %(message)s")
|
||||
sh.setFormatter(fmt)
|
||||
log.addHandler(sh)
|
||||
|
||||
fh = logging.FileHandler(filename=logfile)
|
||||
fh.setLevel(logging.DEBUG)
|
||||
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||
fh.setFormatter(fmt)
|
||||
log.addHandler(fh)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# parse the arguments
|
||||
opts = get_parser().parse_args()
|
||||
|
||||
if opts.showver:
|
||||
print(VERSION)
|
||||
sys.exit(0)
|
||||
|
||||
logpath = os.path.abspath(os.path.dirname(opts.logfile))
|
||||
if not os.path.isdir(logpath):
|
||||
os.makedirs(logpath)
|
||||
setup_logging(opts.logfile)
|
||||
log.debug("opts=%s", opts)
|
||||
|
||||
errors = []
|
||||
|
||||
# Check to see if the socket exists and can be accessed
|
||||
if not os.path.exists(opts.socket):
|
||||
errors.append("%s does not exist" % opts.socket)
|
||||
elif not os.access(opts.socket, os.R_OK|os.W_OK):
|
||||
errors.append("This user cannot access %s" % opts.socket)
|
||||
|
||||
# No point in continuing if there are errors
|
||||
if errors:
|
||||
for e in errors:
|
||||
log.error(e)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(main(opts))
|
27
src/composer/__init__.py
Normal file
27
src/composer/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# composer-cli
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# get composer version
|
||||
try:
|
||||
import composer.version
|
||||
except ImportError:
|
||||
vernum = "devel"
|
||||
else:
|
||||
vernum = composer.version.num
|
49
src/composer/cli/__init__.py
Normal file
49
src/composer/cli/__init__.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# composer-cli
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
log = logging.getLogger("composer-cli")
|
||||
|
||||
from composer.cli.recipes import recipes_cmd
|
||||
from composer.cli.modules import modules_cmd
|
||||
from composer.cli.projects import projects_cmd
|
||||
from composer.cli.compose import compose_cmd
|
||||
|
||||
command_map = {
|
||||
"recipes": recipes_cmd,
|
||||
"modules": modules_cmd,
|
||||
"projects": projects_cmd,
|
||||
"compose": compose_cmd
|
||||
}
|
||||
|
||||
|
||||
def main(opts):
|
||||
""" Main program execution
|
||||
|
||||
:param opts: Cmdline arguments
|
||||
:type opts: argparse.Namespace
|
||||
"""
|
||||
if len(opts.args) > 0 and opts.args[0] in command_map:
|
||||
return command_map[opts.args[0]](opts)
|
||||
elif len(opts.args) == 0:
|
||||
log.error("Unknown command: %s", opts.args)
|
||||
return 1
|
||||
else:
|
||||
log.error("Unknown command: %s", opts.args)
|
||||
return 1
|
26
src/composer/cli/compose.py
Normal file
26
src/composer/cli/compose.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
def compose_cmd(opts):
|
||||
"""Process compose commands
|
||||
|
||||
:param opts: Cmdline arguments
|
||||
:type opts: argparse.Namespace
|
||||
:returns: Value to return from sys.exit()
|
||||
:rtype: int
|
||||
"""
|
||||
return 1
|
26
src/composer/cli/modules.py
Normal file
26
src/composer/cli/modules.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
def modules_cmd(opts):
|
||||
"""Process modules commands
|
||||
|
||||
:param opts: Cmdline arguments
|
||||
:type opts: argparse.Namespace
|
||||
:returns: Value to return from sys.exit()
|
||||
:rtype: int
|
||||
"""
|
||||
return 1
|
26
src/composer/cli/projects.py
Normal file
26
src/composer/cli/projects.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
def projects_cmd(opts):
|
||||
"""Process projects commands
|
||||
|
||||
:param opts: Cmdline arguments
|
||||
:type opts: argparse.Namespace
|
||||
:returns: Value to return from sys.exit()
|
||||
:rtype: int
|
||||
"""
|
||||
return 1
|
519
src/composer/cli/recipes.py
Normal file
519
src/composer/cli/recipes.py
Normal file
@ -0,0 +1,519 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
log = logging.getLogger("composer-cli")
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from composer import http_client as client
|
||||
from composer.cli.utilities import argify, frozen_toml_filename, toml_filename, handle_api_result
|
||||
|
||||
def recipes_cmd(opts):
|
||||
"""Process recipes commands
|
||||
|
||||
:param opts: Cmdline arguments
|
||||
:type opts: argparse.Namespace
|
||||
:returns: Value to return from sys.exit()
|
||||
:rtype: int
|
||||
|
||||
This dispatches the recipes commands to a function
|
||||
"""
|
||||
cmd_map = {
|
||||
"list": recipes_list,
|
||||
"show": recipes_show,
|
||||
"changes": recipes_changes,
|
||||
"diff": recipes_diff,
|
||||
"save": recipes_save,
|
||||
"delete": recipes_delete,
|
||||
"depsolve": recipes_depsolve,
|
||||
"push": recipes_push,
|
||||
"freeze": recipes_freeze,
|
||||
"tag": recipes_tag,
|
||||
"undo": recipes_undo,
|
||||
"workspace": recipes_workspace
|
||||
}
|
||||
return cmd_map[opts.args[1]](opts.socket, opts.api_version, opts.args[2:], opts.json)
|
||||
|
||||
def recipes_list(socket_path, api_version, args, show_json=False):
|
||||
"""Output the list of available recipes
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes list
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/list")
|
||||
result = client.get_url_json(socket_path, api_route)
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
return 0
|
||||
|
||||
print("Recipes: " + ", ".join([r for r in result["recipes"]]))
|
||||
|
||||
return 0
|
||||
|
||||
def recipes_show(socket_path, api_version, args, show_json=False):
|
||||
"""Show the recipes, in TOML format
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes show <recipe,...> Display the recipe in TOML format.
|
||||
|
||||
Multiple recipes will be separated by \n\n
|
||||
"""
|
||||
for recipe in argify(args):
|
||||
api_route = client.api_url(api_version, "/recipes/info/%s?format=toml" % recipe)
|
||||
print(client.get_url_raw(socket_path, api_route) + "\n\n")
|
||||
|
||||
return 0
|
||||
|
||||
def recipes_changes(socket_path, api_version, args, show_json=False):
|
||||
"""Display the changes for each of the recipes
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes changes <recipe,...> Display the changes for each recipe.
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/changes/%s" % (",".join(argify(args))))
|
||||
result = client.get_url_json(socket_path, api_route)
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
return 0
|
||||
|
||||
for recipe in result["recipes"]:
|
||||
print(recipe["name"])
|
||||
for change in recipe["changes"]:
|
||||
prettyCommitDetails(change)
|
||||
|
||||
return 0
|
||||
|
||||
def prettyCommitDetails(change, indent=4):
|
||||
"""Print the recipe's change in a nice way
|
||||
|
||||
:param change: The individual recipe change dict
|
||||
:type change: dict
|
||||
:param indent: Number of spaces to indent
|
||||
:type indent: int
|
||||
"""
|
||||
def revision():
|
||||
if change["revision"]:
|
||||
return " revision %d" % change["revision"]
|
||||
else:
|
||||
return ""
|
||||
|
||||
print " " * indent + change["timestamp"] + " " + change["commit"] + revision()
|
||||
print " " * indent + change["message"] + "\n"
|
||||
|
||||
def recipes_diff(socket_path, api_version, args, show_json=False):
|
||||
"""Display the differences between 2 versions of a recipe
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes diff <recipe-name> Display the differences between 2 versions of a recipe.
|
||||
<from-commit> Commit hash or NEWEST
|
||||
<to-commit> Commit hash, NEWEST, or WORKSPACE
|
||||
"""
|
||||
if len(args) == 0:
|
||||
log.error("recipes diff is missing the recipe name, from commit, and to commit")
|
||||
return 1
|
||||
elif len(args) == 1:
|
||||
log.error("recipes diff is missing the from commit, and the to commit")
|
||||
return 1
|
||||
elif len(args) == 2:
|
||||
log.error("recipes diff is missing the to commit")
|
||||
return 1
|
||||
|
||||
api_route = client.api_url(api_version, "/recipes/diff/%s/%s/%s" % (args[0], args[1], args[2]))
|
||||
result = client.get_url_json(socket_path, api_route)
|
||||
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
return 0
|
||||
|
||||
if result.get("error", False):
|
||||
log.error(result["error"]["msg"])
|
||||
return 1
|
||||
|
||||
for diff in result["diff"]:
|
||||
print(prettyDiffEntry(diff))
|
||||
|
||||
return 0
|
||||
|
||||
def prettyDiffEntry(diff):
|
||||
"""Generate nice diff entry string.
|
||||
|
||||
:param diff: Difference entry dict
|
||||
:type diff: dict
|
||||
:returns: Nice string
|
||||
"""
|
||||
def change(diff):
|
||||
if diff["old"] and diff["new"]:
|
||||
return "Changed"
|
||||
elif diff["new"] and not diff["old"]:
|
||||
return "Added"
|
||||
elif diff["old"] and not diff["new"]:
|
||||
return "Removed"
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def name(diff):
|
||||
if diff["old"]:
|
||||
return diff["old"].keys()[0]
|
||||
elif diff["new"]:
|
||||
return diff["new"].keys()[0]
|
||||
else:
|
||||
return "Unknown"
|
||||
|
||||
def details(diff):
|
||||
if change(diff) == "Changed":
|
||||
if name(diff) == "Description":
|
||||
return '"%s" -> "%s"' % (diff["old"][name(diff)], diff["old"][name(diff)])
|
||||
elif name(diff) == "Version":
|
||||
return "%s -> %s" % (diff["old"][name(diff)], diff["old"][name(diff)])
|
||||
elif name(diff) in ["Module", "Package"]:
|
||||
return "%s %s -> %s" % (diff["old"][name(diff)]["name"], diff["old"][name(diff)]["version"],
|
||||
diff["new"][name(diff)]["version"])
|
||||
else:
|
||||
return "Unknown"
|
||||
elif change(diff) == "Added":
|
||||
return " ".join([diff["new"][k] for k in diff["new"]])
|
||||
elif change(diff) == "Removed":
|
||||
return " ".join([diff["old"][k] for k in diff["old"]])
|
||||
|
||||
return change(diff) + " " + name(diff) + " " + details(diff)
|
||||
|
||||
def recipes_save(socket_path, api_version, args, show_json=False):
|
||||
"""Save the recipe to a TOML file
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes save <recipe,...> Save the recipe to a file, <recipe-name>.toml
|
||||
"""
|
||||
for recipe in argify(args):
|
||||
api_route = client.api_url(api_version, "/recipes/info/%s?format=toml" % recipe)
|
||||
recipe_toml = client.get_url_raw(socket_path, api_route)
|
||||
open(toml_filename(recipe), "w").write(recipe_toml)
|
||||
|
||||
return 0
|
||||
|
||||
def recipes_delete(socket_path, api_version, args, show_json=False):
|
||||
"""Delete a recipe from the server
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
delete <recipe> Delete a recipe from the server
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/delete/%s" % args[0])
|
||||
result = client.delete_url_json(socket_path, api_route)
|
||||
|
||||
return handle_api_result(result, show_json)
|
||||
|
||||
def recipes_depsolve(socket_path, api_version, args, show_json=False):
|
||||
"""Display the packages needed to install the recipe
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes depsolve <recipe,...> Display the packages needed to install the recipe.
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/depsolve/%s" % (",".join(argify(args))))
|
||||
result = client.get_url_json(socket_path, api_route)
|
||||
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
return 0
|
||||
|
||||
for recipe in result["recipes"]:
|
||||
if recipe["recipe"].get("version", ""):
|
||||
print("Recipe: %s v%s" % (recipe["recipe"]["name"], recipe["recipe"]["version"]))
|
||||
else:
|
||||
print("Recipe: %s" % (recipe["recipe"]["name"]))
|
||||
for dep in recipe["dependencies"]:
|
||||
print(" " + packageNEVRA(dep))
|
||||
|
||||
return 0
|
||||
|
||||
def packageNEVRA(pkg):
|
||||
"""Return the package info as a NEVRA
|
||||
|
||||
:param pkg: The package details
|
||||
:type pkg: dict
|
||||
:returns: name-[epoch:]version-release-arch
|
||||
:rtype: str
|
||||
"""
|
||||
if pkg["epoch"]:
|
||||
return "%s-%s:%s-%s.%s" % (pkg["name"], pkg["epoch"], pkg["version"], pkg["release"], pkg["arch"])
|
||||
else:
|
||||
return "%s-%s-%s.%s" % (pkg["name"], pkg["version"], pkg["release"], pkg["arch"])
|
||||
|
||||
def recipes_push(socket_path, api_version, args, show_json=False):
|
||||
"""Push a recipe TOML file to the server, updating the recipe
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
push <recipe> Push a recipe TOML file to the server.
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/new")
|
||||
rval = 0
|
||||
for recipe in argify(args):
|
||||
if not os.path.exists(recipe):
|
||||
log.error("Missing recipe file: %s", recipe)
|
||||
continue
|
||||
recipe_toml = open(recipe, "r").read()
|
||||
|
||||
result = client.post_url_toml(socket_path, api_route, recipe_toml)
|
||||
if handle_api_result(result, show_json):
|
||||
rval = 1
|
||||
|
||||
return rval
|
||||
|
||||
def recipes_freeze(socket_path, api_version, args, show_json=False):
|
||||
"""Handle the recipes freeze commands
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes freeze <recipe,...> Display the frozen recipe's modules and packages.
|
||||
recipes freeze show <recipe,...> Display the frozen recipe in TOML format.
|
||||
recipes freeze save <recipe,...> Save the frozen recipe to a file, <recipe-name>.frozen.toml.
|
||||
"""
|
||||
if args[0] == "show":
|
||||
return recipes_freeze_show(socket_path, api_version, args[1:], show_json)
|
||||
elif args[0] == "save":
|
||||
return recipes_freeze_save(socket_path, api_version, args[1:], show_json)
|
||||
|
||||
if len(args) == 0:
|
||||
log.error("freeze is missing the recipe name")
|
||||
return 1
|
||||
|
||||
api_route = client.api_url(api_version, "/recipes/freeze/%s" % (",".join(argify(args))))
|
||||
result = client.get_url_json(socket_path, api_route)
|
||||
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
else:
|
||||
for entry in result["recipes"]:
|
||||
recipe = entry["recipe"]
|
||||
if recipe.get("version", ""):
|
||||
print("Recipe: %s v%s" % (recipe["name"], recipe["version"]))
|
||||
else:
|
||||
print("Recipe: %s" % (recipe["name"]))
|
||||
|
||||
for m in recipe["modules"]:
|
||||
print(" %s-%s" % (m["name"], m["version"]))
|
||||
|
||||
for p in recipe["packages"]:
|
||||
print(" %s-%s" % (p["name"], p["version"]))
|
||||
|
||||
# Print any errors
|
||||
for err in result.get("errors", []):
|
||||
log.error("%s: %s", err["recipe"], err["msg"])
|
||||
|
||||
# Return a 1 if there are any errors
|
||||
if result.get("errors", []):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def recipes_freeze_show(socket_path, api_version, args, show_json=False):
|
||||
"""Show the frozen recipe in TOML format
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes freeze show <recipe,...> Display the frozen recipe in TOML format.
|
||||
"""
|
||||
if len(args) == 0:
|
||||
log.error("freeze show is missing the recipe name")
|
||||
return 1
|
||||
|
||||
for recipe in argify(args):
|
||||
api_route = client.api_url(api_version, "/recipes/freeze/%s?format=toml" % recipe)
|
||||
print(client.get_url_raw(socket_path, api_route))
|
||||
|
||||
return 0
|
||||
|
||||
def recipes_freeze_save(socket_path, api_version, args, show_json=False):
|
||||
"""Save the frozen recipe to a TOML file
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes freeze save <recipe,...> Save the frozen recipe to a file, <recipe-name>.frozen.toml.
|
||||
"""
|
||||
if len(args) == 0:
|
||||
log.error("freeze save is missing the recipe name")
|
||||
return 1
|
||||
|
||||
for recipe in argify(args):
|
||||
api_route = client.api_url(api_version, "/recipes/freeze/%s?format=toml" % recipe)
|
||||
recipe_toml = client.get_url_raw(socket_path, api_route)
|
||||
open(frozen_toml_filename(recipe), "w").write(recipe_toml)
|
||||
|
||||
return 0
|
||||
|
||||
def recipes_tag(socket_path, api_version, args, show_json=False):
|
||||
"""Tag the most recent recipe commit as a release
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes tag <recipe> Tag the most recent recipe commit as a release.
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/tag/%s" % args[0])
|
||||
result = client.post_url(socket_path, api_route, "")
|
||||
|
||||
return handle_api_result(result, show_json)
|
||||
|
||||
def recipes_undo(socket_path, api_version, args, show_json=False):
|
||||
"""Undo changes to a recipe
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes undo <recipe> <commit> Undo changes to a recipe by reverting to the selected commit.
|
||||
"""
|
||||
if len(args) == 0:
|
||||
log.error("undo is missing the recipe name and commit hash")
|
||||
return 1
|
||||
elif len(args) == 1:
|
||||
log.error("undo is missing commit hash")
|
||||
return 1
|
||||
|
||||
api_route = client.api_url(api_version, "/recipes/undo/%s/%s" % (args[0], args[1]))
|
||||
result = client.post_url(socket_path, api_route, "")
|
||||
|
||||
return handle_api_result(result, show_json)
|
||||
|
||||
def recipes_workspace(socket_path, api_version, args, show_json=False):
|
||||
"""Push the recipe TOML to the temporary workspace storage
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param api_version: Version of the API to talk to. eg. "0"
|
||||
:type api_version: str
|
||||
:param args: List of remaining arguments from the cmdline
|
||||
:type args: list of str
|
||||
:param show_json: Set to True to show the JSON output instead of the human readable output
|
||||
:type show_json: bool
|
||||
|
||||
recipes workspace <recipe> Push the recipe TOML to the temporary workspace storage.
|
||||
"""
|
||||
api_route = client.api_url(api_version, "/recipes/workspace")
|
||||
rval = 0
|
||||
for recipe in argify(args):
|
||||
if not os.path.exists(recipe):
|
||||
log.error("Missing recipe file: %s", recipe)
|
||||
continue
|
||||
recipe_toml = open(recipe, "r").read()
|
||||
|
||||
result = client.post_url_toml(socket_path, api_route, recipe_toml)
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
elif result.get("error", False):
|
||||
log.error(result["error"]["msg"])
|
||||
|
||||
# Any errors results in returning a 1, but we continue with the rest first
|
||||
if not result.get("status", False):
|
||||
rval = 1
|
||||
|
||||
return rval
|
70
src/composer/cli/utilities.py
Normal file
70
src/composer/cli/utilities.py
Normal file
@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
log = logging.getLogger("composer-cli")
|
||||
|
||||
import json
|
||||
|
||||
def argify(args):
|
||||
"""Take a list of human args and return a list with each item
|
||||
|
||||
:param args: list of strings with possible commas and spaces
|
||||
:type args: list of str
|
||||
:returns: List of all the items
|
||||
:rtype: list of str
|
||||
|
||||
Examples:
|
||||
|
||||
["one,two", "three", ",four", ",five,"] returns ["one", "two", "three", "four", "five"]
|
||||
"""
|
||||
return filter(lambda i: i, [arg for entry in args for arg in entry.split(",")])
|
||||
|
||||
def toml_filename(recipe_name):
|
||||
"""Convert a recipe name into a filename.toml
|
||||
|
||||
:param recipe_name: The recipe's name
|
||||
:type recipe_name: str
|
||||
:returns: The recipe name with ' ' converted to - and .toml appended
|
||||
:rtype: str
|
||||
"""
|
||||
return recipe_name.replace(" ", "-") + ".toml"
|
||||
|
||||
def frozen_toml_filename(recipe_name):
|
||||
"""Convert a recipe name into a filename.toml
|
||||
|
||||
:param recipe_name: The recipe's name
|
||||
:type recipe_name: str
|
||||
:returns: The recipe name with ' ' converted to - and .toml appended
|
||||
:rtype: str
|
||||
"""
|
||||
return recipe_name.replace(" ", "-") + ".frozen.toml"
|
||||
|
||||
def handle_api_result(result, show_json=False):
|
||||
"""Log any errors, return the correct value
|
||||
|
||||
:param result: JSON result from the http query
|
||||
:type result: dict
|
||||
"""
|
||||
if show_json:
|
||||
print(json.dumps(result, indent=4))
|
||||
elif result.get("error", False):
|
||||
log.error(result["error"]["msg"])
|
||||
|
||||
if result["status"] == True:
|
||||
return 0
|
||||
else:
|
||||
return 1
|
106
src/composer/http_client.py
Normal file
106
src/composer/http_client.py
Normal file
@ -0,0 +1,106 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
log = logging.getLogger("composer-cli")
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
from composer.unix_socket import UnixHTTPConnectionPool
|
||||
|
||||
def api_url(api_version, url):
|
||||
"""Return the versioned path to the API route
|
||||
|
||||
"""
|
||||
return os.path.normpath("/api/v%s/%s" % (api_version, url))
|
||||
|
||||
def get_url_raw(socket_path, url):
|
||||
"""Return the raw results of a GET request
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param url: URL to request
|
||||
:type url: str
|
||||
:returns: The raw response from the server
|
||||
:rtype: str
|
||||
"""
|
||||
http = UnixHTTPConnectionPool(socket_path)
|
||||
r = http.request("GET", url)
|
||||
return r.data.decode('utf-8')
|
||||
|
||||
def get_url_json(socket_path, url):
|
||||
"""Return the JSON results of a GET request
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param url: URL to request
|
||||
:type url: str
|
||||
:returns: The json response from the server
|
||||
:rtype: dict
|
||||
"""
|
||||
http = UnixHTTPConnectionPool(socket_path)
|
||||
r = http.request("GET", url)
|
||||
return json.loads(r.data.decode('utf-8'))
|
||||
|
||||
def delete_url_json(socket_path, url):
|
||||
"""Send a DELETE request to the url and return JSON response
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param url: URL to send DELETE to
|
||||
:type url: str
|
||||
:returns: The json response from the server
|
||||
:rtype: dict
|
||||
"""
|
||||
http = UnixHTTPConnectionPool(socket_path)
|
||||
r = http.request("DELETE", url)
|
||||
return json.loads(r.data.decode("utf-8"))
|
||||
|
||||
def post_url(socket_path, url, body):
|
||||
"""POST raw data to the URL
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param url: URL to send POST to
|
||||
:type url: str
|
||||
:param body: The data for the body of the POST
|
||||
:type body: str
|
||||
:returns: The json response from the server
|
||||
:rtype: dict
|
||||
"""
|
||||
http = UnixHTTPConnectionPool(socket_path)
|
||||
r = http.request("POST", url,
|
||||
body=body.encode("utf-8"))
|
||||
return json.loads(r.data.decode("utf-8"))
|
||||
|
||||
def post_url_toml(socket_path, url, body):
|
||||
"""POST a TOML recipe to the URL
|
||||
|
||||
:param socket_path: Path to the Unix socket to use for API communication
|
||||
:type socket_path: str
|
||||
:param url: URL to send POST to
|
||||
:type url: str
|
||||
:param body: The data for the body of the POST
|
||||
:type body: str
|
||||
:returns: The json response from the server
|
||||
:rtype: dict
|
||||
"""
|
||||
http = UnixHTTPConnectionPool(socket_path)
|
||||
r = http.request("POST", url,
|
||||
body=body.encode("utf-8"),
|
||||
headers={"Content-Type": "text/x-toml"})
|
||||
return json.loads(r.data.decode("utf-8"))
|
66
src/composer/unix_socket.py
Normal file
66
src/composer/unix_socket.py
Normal file
@ -0,0 +1,66 @@
|
||||
#
|
||||
# Copyright (C) 2018 Red Hat, Inc.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import httplib
|
||||
import socket
|
||||
import urllib3
|
||||
|
||||
|
||||
# These 2 classes were adapted and simplified for use with just urllib3.
|
||||
# Originally from https://github.com/msabramo/requests-unixsocket/blob/master/requests_unixsocket/adapters.py
|
||||
|
||||
# The following was adapted from some code from docker-py
|
||||
# https://github.com/docker/docker-py/blob/master/docker/transport/unixconn.py
|
||||
class UnixHTTPConnection(httplib.HTTPConnection, object):
|
||||
|
||||
def __init__(self, socket_path, timeout=60):
|
||||
"""Create an HTTP connection to a unix domain socket
|
||||
|
||||
:param socket_path: The path to the Unix domain socket
|
||||
:param timeout: Number of seconds to timeout the connection
|
||||
"""
|
||||
super(UnixHTTPConnection, self).__init__('localhost', timeout=timeout)
|
||||
self.socket_path = socket_path
|
||||
self.sock = None
|
||||
|
||||
def __del__(self): # base class does not have d'tor
|
||||
if self.sock:
|
||||
self.sock.close()
|
||||
|
||||
def connect(self):
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect(self.socket_path)
|
||||
self.sock = sock
|
||||
|
||||
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
|
||||
|
||||
def __init__(self, socket_path, timeout=60):
|
||||
"""Create a connection pool using a Unix domain socket
|
||||
|
||||
:param socket_path: The path to the Unix domain socket
|
||||
:param timeout: Number of seconds to timeout the connection
|
||||
"""
|
||||
super(UnixHTTPConnectionPool, self).__init__('localhost', timeout=timeout)
|
||||
self.socket_path = socket_path
|
||||
|
||||
def _new_conn(self):
|
||||
return UnixHTTPConnection(self.socket_path, self.timeout)
|
||||
|
||||
if __name__ == '__main__':
|
||||
http = UnixHTTPConnectionPool("/var/run/weldr/api.socket")
|
||||
r = http.request("GET", "/api/v0/recipes/list")
|
||||
print(r.data)
|
@ -28,6 +28,7 @@ class LoraxLintConfig(PocketLintConfig):
|
||||
FalsePositive(r"Instance of 'int' has no .* member"),
|
||||
FalsePositive(r"Catching too general exception Exception"),
|
||||
FalsePositive(r"^E0712.*: Catching an exception which doesn't inherit from (Base|)Exception: GError$"),
|
||||
FalsePositive(r"Module 'composer' has no 'version' member"),
|
||||
]
|
||||
|
||||
@property
|
||||
|
Loading…
Reference in New Issue
Block a user