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
|
*.pyc
|
||||||
src/pylorax/version.py*
|
src/pylorax/version.py*
|
||||||
|
src/composer/version.py*
|
||||||
*.swp
|
*.swp
|
||||||
.pylint.d/
|
.pylint.d/
|
||||||
_build/
|
_build/
|
||||||
|
6
Makefile
6
Makefile
@ -11,10 +11,13 @@ USER_SITE_PACKAGES ?= $(shell sudo $(PYTHON) -m site --user-site)
|
|||||||
|
|
||||||
default: all
|
default: all
|
||||||
|
|
||||||
|
src/composer/version.py: lorax.spec
|
||||||
|
echo "num = '$(VERSION)-$(RELEASE)'" > src/composer/version.py
|
||||||
|
|
||||||
src/pylorax/version.py: lorax.spec
|
src/pylorax/version.py: lorax.spec
|
||||||
echo "num = '$(VERSION)-$(RELEASE)'" > src/pylorax/version.py
|
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
|
$(PYTHON) setup.py build
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
@ -42,6 +45,7 @@ test: docs
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
-rm -rf build src/pylorax/version.py
|
-rm -rf build src/pylorax/version.py
|
||||||
|
-rm -rf build src/composer/version.py
|
||||||
|
|
||||||
tag:
|
tag:
|
||||||
git tag -f $(TAG)
|
git tag -f $(TAG)
|
||||||
|
13
lorax.spec
13
lorax.spec
@ -100,6 +100,16 @@ BuildRequires: systemd
|
|||||||
%description composer
|
%description composer
|
||||||
lorax-composer provides a REST API for building images using lorax.
|
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
|
%prep
|
||||||
%setup -q
|
%setup -q
|
||||||
|
|
||||||
@ -148,6 +158,9 @@ getent passwd weldr >/dev/null 2>&1 || useradd -r -g weldr -d / -s /sbin/nologin
|
|||||||
%{_sbindir}/lorax-composer
|
%{_sbindir}/lorax-composer
|
||||||
%{_unitdir}/lorax-composer.service
|
%{_unitdir}/lorax-composer.service
|
||||||
|
|
||||||
|
%files -n composer-cli
|
||||||
|
%{_bindir}/composer-cli
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Thu Feb 22 2018 Brian C. Lane <bcl@redhat.com> 19.7.10-1
|
* 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)
|
- 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",
|
data_files.append(("/usr/sbin", ["src/sbin/lorax", "src/sbin/mkefiboot",
|
||||||
"src/sbin/livemedia-creator", "src/sbin/lorax-composer"]))
|
"src/sbin/livemedia-creator", "src/sbin/lorax-composer"]))
|
||||||
data_files.append(("/usr/bin", ["src/bin/image-minimizer",
|
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
|
# get the version
|
||||||
sys.path.insert(0, "src")
|
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"Instance of 'int' has no .* member"),
|
||||||
FalsePositive(r"Catching too general exception Exception"),
|
FalsePositive(r"Catching too general exception Exception"),
|
||||||
FalsePositive(r"^E0712.*: Catching an exception which doesn't inherit from (Base|)Exception: GError$"),
|
FalsePositive(r"^E0712.*: Catching an exception which doesn't inherit from (Base|)Exception: GError$"),
|
||||||
|
FalsePositive(r"Module 'composer' has no 'version' member"),
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
Loading…
Reference in New Issue
Block a user