Skip to content

Instantly share code, notes, and snippets.

@tbielawa
Created February 25, 2014 03:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbielawa/9201843 to your computer and use it in GitHub Desktop.
Save tbielawa/9201843 to your computer and use it in GitHub Desktop.
diff --git a/Makefile b/Makefile
index be57177..5a59b31 100644
--- a/Makefile
+++ b/Makefile
@@ -171,6 +171,7 @@ rpm: rpmcommon
@find rpm-build -maxdepth 2 -name 'juicer*.rpm' | awk '{print " " $$1}'
@echo "#############################################"
+# This makes an RPM for Openshift Online
oorpm: rpmcommon
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
--define "_builddir %{_topdir}" \
@@ -191,7 +192,8 @@ koji: srpm
test:
. ./hacking/setup-env
if [ "$(LOG)" = "true" ]; then \
- ./hacking/tests | tee /tmp/juicer_tests.log; \
+ ./hacking/tests | tee -a /tmp/juicer_tests.log; \
+ echo "Test results logged to /tmp/juicer_tests.log"; \
else \
./hacking/tests; \
fi
diff --git a/bin/juicer b/bin/juicer
index d4ee78d..eddd743 100755
--- a/bin/juicer
+++ b/bin/juicer
@@ -32,4 +32,7 @@ if __name__ == '__main__':
try:
main()
except JuicerError, e:
+ print "Juicer error happened:"
print e
+ except KeyboardInterrupt:
+ print "User killed via ^C"
diff --git a/bin/juicer-admin b/bin/juicer-admin
index aa2e468..0d7eeb8 100755
--- a/bin/juicer-admin
+++ b/bin/juicer-admin
@@ -31,6 +31,7 @@ if __name__ == '__main__':
try:
main()
except JuicerError, e:
+ print "Juicer error happened:"
print e
- except KeyError, e:
- pass
+ except KeyboardInterrupt:
+ print "User killed via ^C"
diff --git a/docs/man/man1/juicer-admin.1 b/docs/man/man1/juicer-admin.1
index ab773c8..8324fe1 100644
--- a/docs/man/man1/juicer-admin.1
+++ b/docs/man/man1/juicer-admin.1
@@ -2,12 +2,12 @@
.\" Title: juicer-admin
.\" Author: :doctype:manpage
.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
-.\" Date: 02/14/2014
+.\" Date: 02/19/2014
.\" Manual: Pulp repos and release carts
.\" Source: Juicer 0.7.0
.\" Language: English
.\"
-.TH "JUICER\-ADMIN" "1" "02/14/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
+.TH "JUICER\-ADMIN" "1" "02/19/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
@@ -40,7 +40,9 @@ Manage pulp repositories, users, and roles\&.
\fB\-v\fR, \fB\-\-verbose\fR
.RS 4
Increase the verbosity (up to 3x)\&. In usage specify
-\fI\-v\fR\fBbefore\fR\fICOMMAND\fR\&.
+\fI\-v\fR
+before
+\fICOMMAND\fR\&.
.RE
.PP
\fB\-V\fR, \fB\-\-version\fR
@@ -52,7 +54,8 @@ Print the version that you\(cqre using of juicer\-admin\&.
.RS 4
Specify which environment(s) to perform the
\fISUBCOMMAND\fR
-in\&. In usage all
+in\&. In
+\fBjuicer\-admin\fR(1) usage, all
\fISUBCOMMAND\fRs accept this option when given after
\fISUBCOMMAND\fR\&.
.RE
@@ -61,12 +64,12 @@ in\&. In usage all
This section describes all of the \fIREPO\fR subcommands\&.
.SH "REPO CREATE"
.sp
-usage: juicer\-admin repo create REPONAME [\-\-arch ARCH] [\-\-feed FEED] [\-\-checksum\-type TYPE]
+usage: juicer\-admin repo create \fIREPONAME\fR [\-\-arch \fIARCH\fR] [\-\-feed \fIFEED\fR] [\-\-checksum\-type \fITYPE\fR]
.PP
\fBREPONAME\fR
.RS 4
The name of the repository to be created\&. Only alphanumeric, \&., \-, and _ allowed\&. As a regular expression:
-\fB([a\-zA\-Z\-_\&.]+)\fR
+\fB([a\-zA\-Z0\-9\-_\&.]+)\fR
.RE
.PP
\fB\-\-arch=\fR\fIARCH\fR
@@ -89,6 +92,25 @@ Optional\&. The checksum type to use when generating meta\-data\&. Defaults to
\fBsha256\fR, also valid is
\fBsha\fR\&.
.RE
+.SH "REPO IMPORT"
+.sp
+usage: juicer\-admin repo import \-\-from\-file \fIREPO_DEF\fR [\-\-noop]
+.sp
+Create repositories matching the definitions in the repo def file\&. Repositories which already exist will be updated\&.
+.PP
+\fB\-\-from\-file=\fR\fIREPO_DEF\fR
+.RS 4
+Repository definition file in JSON format\&. See
+\fIREPO DEF FORMAT\fR
+(below) for a quick review of the syntax\&. See the docs online or in /usr/share/doc/juicer*/repo_syntax\&.md for a detailed description\&.
+.RE
+.PP
+\fB\-\-noop\fR, \fB\-\-dry\-run\fR, \fB\-n\fR
+.RS 4
+Don\(cqt create the repos, just show what would have happened\&.
+.RE
+.sp
+\fBNote:\fR This command does not respect \fB\-\-in\fR
.SH "REPO DELETE"
.sp
usage: juicer\-admin repo delete \fIREPONAME\fR
@@ -99,16 +121,30 @@ Name of repository which will be deleted
.RE
.SH "REPO LIST"
.sp
-usage: juicer\-admin repo list
+usage: juicer\-admin repo list [\-\-json]
+.sp
+List all of the repos in any or all environments\&.
+.PP
+\fB\-\-json\fR
+.RS 4
+Optional\&. Return the data in JSON format\&.
+.RE
.SH "REPO SHOW"
.sp
-usage: juicer\-admin repo show \fIREPONAME\fR
+usage: juicer\-admin repo show \fIREPONAME\fR [\&...] [\-\-json]
.sp
-Show basic statistics about a repo in pulp\&. Currently this command just prints the number of packages in the specified repository\&.
+Show basic statistics in table format about one or more repos in pulp\&. This information includes: Name, Environment, RPM count, SRPM count, and Checksum Type\&.
.PP
\fBREPONAME\fR
.RS 4
-Name of the repository to show
+Name of the repository to show\&. Separate multiple
+\fIREPONAME\fR
+parameters with spaces\&.
+.RE
+.PP
+\fB\-\-json\fR
+.RS 4
+Optional\&. Return the data in JSON format\&.
.RE
.SH "REPO SYNC"
.sp
@@ -123,7 +159,7 @@ Name of repository to sync
This section describes all of the \fIUSER\fR subcommands\&.
.SH "USER CREATE"
.sp
-usage: juicer\-admin user create LOGIN \-\-name FULLNAME \-\-password PASSWORD
+usage: juicer\-admin user create \fILOGIN\fR \-\-name \fIFULLNAME\fR \-\-password \fIPASSWORD\fR
.sp
Create a user in the pulp system\&.
.PP
@@ -201,6 +237,146 @@ Login or username of user which will be added to role
.RE
.sp
See the Pulp User documentation (\fBSEE ALSO\fR) for more information on the specifics of role management\&.
+.SH "REPO DEF FORMAT"
+.sp
+\fBMandatory keys\fR:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+name (string)
+.RE
+.sp
+\fBOptional Keys\fR:
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+feed (string)
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+checksum_type (string, one of:
+\fBsha\fR,
+\fBsha256\fR)
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+env (list of environment name strings)
+.RE
+.sp
+\fBExample:\fR
+.sp
+.if n \{\
+.RS 4
+.\}
+.nf
+[
+ {"name": "repo01", "env": ["prod"]},
+ {"name": "repo02"},
+ {
+ "name": "fedora_mirror",
+ "feed": "http://download\&.fedoraproject\&.org/pub/fedora/linux/releases/20/Everything/x86_64/os/",
+ "checksum_type": "sha",
+ "env": ["dev", "prod"]
+ }
+]
+.fi
+.if n \{\
+.RE
+.\}
+.sp
+\fBProtips\fR
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+Don\(cqt end lists or hashes with trailing commas
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+Remember to close all of your braces and brackets: Each
+\fB[\fR
+has a matching
+\fB]\fR, each
+\fB{\fR
+has a matching
+\fB}\fR
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+Use a javascript mode in your editor if it doesn\(cqt have a native json mode
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+Setting
+\fBenv\fR
+to an empty list (\fB[]\fR) will
+\fInot\fR
+delete the repo from any environment
+.RE
+.sp
+.RS 4
+.ie n \{\
+\h'-04'\(bu\h'+03'\c
+.\}
+.el \{\
+.sp -1
+.IP \(bu 2.3
+.\}
+Use a linting service if you\(cqre stuck, for example:
+\fBhttp://jsonlint\&.com/\fR
+.RE
.SH "FILES"
.sp
\fB~/\&.config/juicer/config\fR \(em Juicer configuration file
@@ -213,16 +389,18 @@ Juicer was written by GCA\-PC, Red Hat, Inc\&.\&.
This man page was written by Tim Bielawa <tbielawa@redhat\&.com> and Andrew Butcher <abutcher@redhat\&.com>\&.
.SH "COPYRIGHT"
.sp
-Copyright \(co 2012, Red Hat, Inc\&.\&.
+Copyright \(co 2012\-2014, Red Hat, Inc\&.\&.
.sp
Juicer is released under the terms of the GPLv3+ License\&.
.SH "SEE ALSO"
.sp
\fBjuicer\fR(1), \fBjuicer\&.conf\fR(5)
.sp
-\fBPulp User Documentation\fR \(em https://pulp\-user\-guide\&.readthedocs\&.org/en/pulp\-2\&.0/
+\fBPulp User Documentation\fR \(em http://www\&.pulpproject\&.org/docs/
+.sp
+\fBDetailed Repo Def Description\fR \(em https://github\&.com/juicer/juicer/blob/master/docs/markdown/repo_syntax\&.md
.sp
-The Juicer Homepage: https://github\&.com/juicer/juicer/
+\fBThe Juicer Homepage\fR \(em https://github\&.com/juicer/juicer/
.SH "AUTHOR"
.PP
\fB:doctype:manpage\fR
diff --git a/docs/man/man1/juicer-admin.1.asciidoc.in b/docs/man/man1/juicer-admin.1.asciidoc.in
index 4faf859..a918d5b 100644
--- a/docs/man/man1/juicer-admin.1.asciidoc.in
+++ b/docs/man/man1/juicer-admin.1.asciidoc.in
@@ -26,15 +26,16 @@ COMMON OPTIONS
--------------
*-v*, *--verbose*::
-Increase the verbosity (up to 3x). In usage specify '-v' *before* 'COMMAND'.
+Increase the verbosity (up to 3x). In usage specify '-v' before 'COMMAND'.
*-V*, *--version*::
Print the version that you're using of juicer-admin.
*--in* 'env' ...::
-Specify which environment(s) to perform the 'SUBCOMMAND' in. In usage
-all __SUBCOMMAND__s accept this option when given after 'SUBCOMMAND'.
+Specify which environment(s) to perform the 'SUBCOMMAND' in. In
+*juicer-admin*(1) usage, all __SUBCOMMAND__s accept this option when
+given after 'SUBCOMMAND'.
@@ -45,17 +46,16 @@ This section describes all of the __REPO__ subcommands.
REPO CREATE
-----------
-usage: juicer-admin repo create REPONAME [--arch ARCH] [--feed FEED] [--checksum-type TYPE]
+usage: juicer-admin repo create 'REPONAME' [--arch 'ARCH'] [--feed 'FEED'] [--checksum-type 'TYPE']
*REPONAME*::
The name of the repository to be created. Only alphanumeric, ., -, and
-_ allowed. As a regular expression: *([a-zA-Z-_.]+)*
+_ allowed. As a regular expression: *([a-zA-Z0-9-_.]+)*
*--arch=*'ARCH'::
Optional. Repository package architecture. Defaults to *noarch*. Other
values might be: *i386*, *i686*, or *x86_64*.
-
*--feed=*'FEED'::
Optional. A feed url from which to synchronize yum repository packages.
@@ -64,6 +64,25 @@ Optional. The checksum type to use when generating meta-data.
Defaults to *sha256*, also valid is *sha*.
+REPO IMPORT
+-----------
+usage: juicer-admin repo import --from-file 'REPO_DEF' [--noop]
+
+Create repositories matching the definitions in the repo def
+file. Repositories which already exist will be updated.
+
+*--from-file=*'REPO_DEF'::
+
+Repository definition file in JSON format. See __REPO DEF FORMAT__
+(below) for a quick review of the syntax. See the docs online or in
+/usr/share/doc/juicer*/repo_syntax.md for a detailed description.
+
+*--noop*, *--dry-run*, *-n*::
+
+Don't create the repos, just show what would have happened.
+
+
+*Note:* This command does not respect *--in*
REPO DELETE
@@ -77,18 +96,30 @@ Name of repository which will be deleted
REPO LIST
---------
-usage: juicer-admin repo list
+usage: juicer-admin repo list [--json]
+
+List all of the repos in any or all environments.
+
+*--json*::
+Optional. Return the data in JSON format.
REPO SHOW
---------
-usage: juicer-admin repo show 'REPONAME'
+usage: juicer-admin repo show 'REPONAME' [...] [--json]
-Show basic statistics about a repo in pulp. Currently this command
-just prints the number of packages in the specified repository.
+Show basic statistics in table format about one or more repos in
+pulp. This information includes: Name, Environment, RPM count, SRPM
+count, and Checksum Type.
*REPONAME*::
-Name of the repository to show
+Name of the repository to show. Separate multiple 'REPONAME'
+parameters with spaces.
+
+*--json*::
+
+Optional. Return the data in JSON format.
+
REPO SYNC
@@ -114,7 +145,7 @@ This section describes all of the __USER__ subcommands.
USER CREATE
-----------
-usage: juicer-admin user create LOGIN --name FULLNAME --password PASSWORD
+usage: juicer-admin user create 'LOGIN' --name 'FULLNAME' --password 'PASSWORD'
Create a user in the pulp system.
@@ -198,7 +229,40 @@ the specifics of role management.
+REPO DEF FORMAT
+---------------
+
+*Mandatory keys*:
+
+- name (string)
+
+*Optional Keys*:
+
+- feed (string)
+- checksum_type (string, one of: *sha*, *sha256*)
+- env (list of environment name strings)
+*Example:*
+
+ [
+ {"name": "repo01", "env": ["prod"]},
+ {"name": "repo02"},
+ {
+ "name": "fedora_mirror",
+ "feed": "http://download.fedoraproject.org/pub/fedora/linux/releases/20/Everything/x86_64/os/",
+ "checksum_type": "sha",
+ "env": ["dev", "prod"]
+ }
+ ]
+
+
+*Protips*
+
+- Don't end lists or hashes with trailing commas
+- Remember to close all of your braces and brackets: Each *[* has a matching *]*, each *{* has a matching *}*
+- Use a javascript mode in your editor if it doesn't have a native json mode
+- Setting *env* to an empty list (*[]*) will 'not' delete the repo from any environment
+- Use a linting service if you're stuck, for example: *http://jsonlint.com/*
@@ -224,7 +288,7 @@ Andrew Butcher <abutcher@redhat.com>.
COPYRIGHT
---------
-Copyright © 2012, Red Hat, Inc..
+Copyright © 2012-2014, Red Hat, Inc..
Juicer is released under the terms of the GPLv3+ License.
@@ -234,6 +298,8 @@ SEE ALSO
--------
*juicer*(1), *juicer.conf*(5)
-*Pulp User Documentation* -- <https://pulp-user-guide.readthedocs.org/en/pulp-2.0/>
+*Pulp User Documentation* -- <http://www.pulpproject.org/docs/>
+
+*Detailed Repo Def Description* -- <https://github.com/juicer/juicer/blob/master/docs/markdown/repo_syntax.md>
-The Juicer Homepage: <https://github.com/juicer/juicer/>
+*The Juicer Homepage* -- <https://github.com/juicer/juicer/>
diff --git a/docs/man/man1/juicer.1 b/docs/man/man1/juicer.1
index 21015dd..4111786 100644
--- a/docs/man/man1/juicer.1
+++ b/docs/man/man1/juicer.1
@@ -2,12 +2,12 @@
.\" Title: juicer
.\" Author: :doctype:manpage
.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
-.\" Date: 02/14/2014
+.\" Date: 02/19/2014
.\" Manual: Pulp repos and release carts
.\" Source: Juicer 0.7.0
.\" Language: English
.\"
-.TH "JUICER" "1" "02/14/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
+.TH "JUICER" "1" "02/19/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
@@ -42,16 +42,30 @@ RPMs (\fIitems\fR) need not be stored on your local machine\&. Juicer can handle
\fB\-v\fR, \fB\-\-verbose\fR
.RS 4
Increase the verbosity (up to 3x)\&. In usage specify
-\fI\-v\fR\fBbefore\fR\fICOMMAND\fR\&.
+\fI\-v\fR
+before
+\fICOMMAND\fR\&.
.RE
.PP
\fB\-V\fR, \fB\-\-version\fR
.RS 4
Print the version that you\(cqre using of juicer\&.
.RE
+.PP
+\fB\-\-in\fR \fIenv\fR \&...
+.RS 4
+In
+\fBjuicer\fR(1) many subcommands allow you to limit their scope to a specific environment\&. Use
+\fB\-\-in\fR
+to specify which environment(s) to perform the
+\fISUBCOMMAND\fR
+in\&. Commands which support this will show
+\fB[\-\-in]\fR
+in their usage line\&.
+.RE
.SH "CART CREATE"
.sp
-usage: juicer cart create \fICARTNAME\fR [\-f manifest] [\-r \fIREPONAME\fR \fIitems\fR \&... [ \-r \fIREPONAME\fR \fIitems\fR ]]
+usage: juicer cart create \fICARTNAME\fR [\-f \fImanifest\fR] [\-r \fIREPONAME\fR \fIitems\fR \&... [ \-r \fIREPONAME\fR \fIitems\fR ]]
.sp
Create a cart with the items specified\&.
.PP
@@ -63,9 +77,17 @@ The name of the new release cart\&.
\fB\-f\fR \fImanifest\fR
.RS 4
Create a cart from a manifest file\&. A manifest file is written in the following format:
-.RE
.sp
+.if n \{\
+.RS 4
+.\}
+.nf
name: version\-release
+.fi
+.if n \{\
+.RE
+.\}
+.RE
.PP
\fB\-r\fR \fIREPONAME\fR
.RS 4
@@ -83,7 +105,7 @@ Items to add to the cart in repository
.RE
.SH "RPM DELETE"
.sp
-usage: juicer rpm delete \-r REPO\-NAME ITEM ITEM \&... \-\-in [ENV \&...]
+usage: juicer rpm delete \-r \fIREPO\-NAME\fR \fIITEM\fR \fIITEM\fR \&... [\-\-in]
.sp
Delete rpms in a repository\&.
.PP
@@ -96,24 +118,14 @@ The name of the repo rpms live in\&.
.RS 4
Filename of rpm to delete\&.
.RE
-.PP
-\fB\-\-in\fR \fIENVIRONMENT\fR \&...
-.RS 4
-The environments to delete rpms in\&.
-.RE
.SH "HELLO"
.sp
-usage: juicer hello [\-\-in \fIENVIRONMENT\fR \&...]
+usage: juicer hello [\-\-in]
.sp
Test connection settings in \fB~/\&.config/juicer/config\fR
-.PP
-\fBENVIRONMENT\fR
-.RS 4
-The environments to limit connecting testing to\&.
-.RE
.SH "CART MERGE"
.sp
-usage: juicer cart merge CART1 [\&...] CARTN \-\-name NEWCARTNAME
+usage: juicer cart merge \fICART1\fR [\&...] \fICARTN\fR \-\-name \fINEWCARTNAME\fR
.sp
Merges the contents of N carts into \fINEWCARTNAME\fR\&. Defaults to updating \fICART1\fR\&.
.PP
@@ -130,7 +142,7 @@ Names of N carts to merge\&.
.sp
usage: juicer cart pull \fICARTNAME\fR
.sp
-Pulls a description of a cart from the pulp server and saves it on your local machine in ~/\&.config/juicer/carts/\&.
+Pulls a description of a cart from the pulp server and saves it on your local machine in \fB~/\&.config/juicer/carts/\fR\&.
.PP
\fBCARTNAME\fR
.RS 4
@@ -138,7 +150,7 @@ The name of the cart to pull\&.
.RE
.SH "CART PUSH"
.sp
-usage: juicer cart push \fICARTNAME\fR [\-\-in \fIENVIRONMENT\fR \&...]
+usage: juicer cart push \fICARTNAME\fR [\-\-in]
.sp
Pushes/Updates a cart on the pulp server\&.
.PP
@@ -146,14 +158,9 @@ Pushes/Updates a cart on the pulp server\&.
.RS 4
The name of the cart to push\&.
.RE
-.PP
-\fB\-\-in\fR \fIENVIRONMENT\fR \&...
-.RS 4
-The environments to push the new/updated cart to\&.
-.RE
.SH "REPO PUBLISH"
.sp
-usage: juicer repo publish \fIREPO\fR [\-\-in \fIENVIRONMENT\fR \&...]
+usage: juicer repo publish \fIREPO\fR [\-\-in]
.sp
Publishes a repository, regenerating it\(cqs metadata\&.
.PP
@@ -161,11 +168,6 @@ Publishes a repository, regenerating it\(cqs metadata\&.
.RS 4
The name of the repo to publish\&.
.RE
-.PP
-\fB\-\-in\fR \fIENVIRONMENT\fR \&...
-.RS 4
-The environments to publish repository in\&.
-.RE
.SH "CART PROMOTE"
.sp
usage: juicer cart promote \fICARTNAME\fR
@@ -192,9 +194,17 @@ The name of the new release cart\&.
\fB\-f\fR \fIMANIFEST\fR
.RS 4
Update a cart with a manifest file\&. A manifest file is written in the following format:
-.RE
.sp
+.if n \{\
+.RS 4
+.\}
+.nf
name: version\-release
+.fi
+.if n \{\
+.RE
+.\}
+.RE
.PP
\fB\-r\fR \fIREPONAME\fR
.RS 4
@@ -212,7 +222,7 @@ Items to add to the cart in repository
.RE
.SH "RPM SEARCH"
.sp
-usage: juicer rpm search \fIITEM\fR [\-r \fIREPO\fR \&...] [\-c] [\-\-in \fIENVIRONMENT\fR \&...]
+usage: juicer rpm search \fIITEM\fR [\-r \fIREPO\fR \&...] [\-c] [\-\-in]
.sp
Search for an RPM (\fIitem\fR) in pulp\&.
.PP
@@ -230,11 +240,6 @@ The repo(s) to limit search scope to\&.
.RS 4
Search for the package in carts as well\&.
.RE
-.PP
-\fB\-\-in\fR \fIENVIRONMENT\fR \&...
-.RS 4
-The environments to limit search scope to\&.
-.RE
.SH "CART SHOW"
.sp
usage: juicer cart show \fICARTNAME\fR
@@ -258,7 +263,7 @@ The pattern to match\&. Default:
.RE
.SH "RPM UPLOAD"
.sp
-usage: juicer rpm upload \-r \fIREPO\fR \fIITEM\fR \&... [\-\-in \fIENVIRONMENT\fR \&...]
+usage: juicer rpm upload \-r \fIREPO\fR \fIITEM\fR \&... [\-\-in]
.sp
Upload multiple RPMs or files (\fIITEM\fR) to \fIREPO\fR\&.
.PP
@@ -275,11 +280,6 @@ option may be given multiple times\&.
.RS 4
Name of the RPM(s) or file(s) to upload\&.
.RE
-.PP
-\fB\-\-in\fR \fIENVIRONMENT\fR \&...
-.RS 4
-The environments which items will be uploaded to\&.
-.RE
.SH "EXAMPLES"
.sp
\fIitems\fR given may be any number and combination of the following input resource types:
@@ -419,14 +419,14 @@ Juicer was written by GCA\-PC, Red Hat, Inc\&.\&.
This man page was written by Tim Bielawa <tbielawa@redhat\&.com>\&.
.SH "COPYRIGHT"
.sp
-Copyright \(co 2012, Red Hat, Inc\&.\&.
+Copyright \(co 2012\-2014, Red Hat, Inc\&.\&.
.sp
Juicer is released under the terms of the GPLv3+ License\&.
.SH "SEE ALSO"
.sp
\fBjuicer\-admin\fR(1), \fBjuicer\&.conf\fR(5), \fBfnmatch\fR(3)
.sp
-The Juicer Homepage: https://github\&.com/juicer/juicer/
+\fBThe Juicer Homepage\fR \(em https://github\&.com/juicer/juicer/
.SH "AUTHOR"
.PP
\fB:doctype:manpage\fR
diff --git a/docs/man/man1/juicer.1.asciidoc.in b/docs/man/man1/juicer.1.asciidoc.in
index 8f36f9b..8340080 100644
--- a/docs/man/man1/juicer.1.asciidoc.in
+++ b/docs/man/man1/juicer.1.asciidoc.in
@@ -36,15 +36,23 @@ COMMON OPTIONS
--------------
*-v*, *--verbose*::
-Increase the verbosity (up to 3x). In usage specify '-v' *before* 'COMMAND'.
+Increase the verbosity (up to 3x). In usage specify '-v' before 'COMMAND'.
*-V*, *--version*::
Print the version that you're using of juicer.
+*--in* 'env' ...::
+
+In *juicer*(1) many subcommands allow you to limit their scope to a
+specific environment. Use *--in* to specify which environment(s) to
+perform the 'SUBCOMMAND' in. Commands which support this will show
+*[--in]* in their usage line.
+
+
CART CREATE
-----------
-usage: juicer cart create 'CARTNAME' [-f manifest] [-r 'REPONAME' 'items' ... [ -r 'REPONAME' 'items' ]]
+usage: juicer cart create 'CARTNAME' [-f 'manifest'] [-r 'REPONAME' 'items' ... [ -r 'REPONAME' 'items' ]]
Create a cart with the items specified.
@@ -55,7 +63,7 @@ The name of the new release cart.
Create a cart from a manifest file. A manifest file is written in
the following format:
-name: version-release
+ name: version-release
*-r* 'REPONAME'::
Name of the reopsitory to install 'ITEMS' into. The '-r' option may be
@@ -64,28 +72,11 @@ given multiple times.
*ITEM* ...::
Items to add to the cart in repository 'REPONAME'.
-////////////////////////////////////////////////////////////////////////
-CREATE-LIKE
------------
-usage: juicer create-like 'CARTNAME' 'OLDCARTNAME' 'ITEM' [ item2 [ ...] ]
-
-Create a new cart based off an existing one.
-
-*CARTNAME*::
-The name of your new release cart.
-
-*OLDCARTNAME*::
-Cart to copy from.
-
-*ITEMS*::
-Items to update in the cart.
-
-////////////////////////////////////////////////////////////////////////
RPM DELETE
----------
-usage: juicer rpm delete -r REPO-NAME ITEM ITEM ... --in [ENV ...]
+usage: juicer rpm delete -r 'REPO-NAME' 'ITEM' 'ITEM' ... [--in]
Delete rpms in a repository.
@@ -95,35 +86,20 @@ The name of the repo rpms live in.
*ITEM*::
Filename of rpm to delete.
-*--in* 'ENVIRONMENT' ... ::
-The environments to delete rpms in.
-////////////////////////////////////////
-EDIT
-----
-usage: juicer edit CARTNAME
-
-Interactively edit a release cart.
-
-*CARTNAME*::
-The name of your release cart.
-////////////////////////////////////////
-
HELLO
------
-usage: juicer hello [--in 'ENVIRONMENT' ...]
+usage: juicer hello [--in]
Test connection settings in *~/.config/juicer/config*
-*ENVIRONMENT*::
-The environments to limit connecting testing to.
CART MERGE
----------
-usage: juicer cart merge CART1 [...] CARTN --name NEWCARTNAME
+usage: juicer cart merge 'CART1' [...] 'CARTN' --name 'NEWCARTNAME'
Merges the contents of N carts into 'NEWCARTNAME'. Defaults to
updating 'CART1'.
@@ -141,7 +117,7 @@ CART PULL
usage: juicer cart pull 'CARTNAME'
Pulls a description of a cart from the pulp server and saves it on
-your local machine in ~/.config/juicer/carts/.
+your local machine in *~/.config/juicer/carts/*.
*CARTNAME*::
The name of the cart to pull.
@@ -150,28 +126,24 @@ The name of the cart to pull.
CART PUSH
---------
-usage: juicer cart push 'CARTNAME' [--in 'ENVIRONMENT' ...]
+usage: juicer cart push 'CARTNAME' [--in]
Pushes/Updates a cart on the pulp server.
*CARTNAME*::
The name of the cart to push.
-*--in* 'ENVIRONMENT' ... ::
-The environments to push the new/updated cart to.
REPO PUBLISH
------------
-usage: juicer repo publish 'REPO' [--in 'ENVIRONMENT' ...]
+usage: juicer repo publish 'REPO' [--in]
Publishes a repository, regenerating it's metadata.
*REPO*::
The name of the repo to publish.
-*--in* 'ENVIRONMENT' ... ::
-The environments to publish repository in.
CART PROMOTE
@@ -201,7 +173,7 @@ The name of the new release cart.
Update a cart with a manifest file. A manifest file is written in
the following format:
-name: version-release
+ name: version-release
*-r* 'REPONAME'::
Name of the reopsitory to install 'ITEMS' into. The '-r' option may be
@@ -213,7 +185,7 @@ Items to add to the cart in repository 'REPONAME'.
RPM SEARCH
----------
-usage: juicer rpm search 'ITEM' [-r 'REPO' ...] [-c] [--in 'ENVIRONMENT' ...]
+usage: juicer rpm search 'ITEM' [-r 'REPO' ...] [-c] [--in]
Search for an RPM ('item') in pulp.
@@ -226,8 +198,6 @@ The repo(s) to limit search scope to.
*-c*::
Search for the package in carts as well.
-*--in* 'ENVIRONMENT' ...::
-The environments to limit search scope to.
@@ -256,7 +226,7 @@ The pattern to match. Default: ***
RPM UPLOAD
----------
-usage: juicer rpm upload -r 'REPO' 'ITEM' ... [--in 'ENVIRONMENT' ...]
+usage: juicer rpm upload -r 'REPO' 'ITEM' ... [--in]
Upload multiple RPMs or files ('ITEM') to 'REPO'.
@@ -267,8 +237,6 @@ multiple times.
*ITEM* ...::
Name of the RPM(s) or file(s) to upload.
-*--in* 'ENVIRONMENT' ...::
-The environments which items will be uploaded to.
@@ -359,7 +327,7 @@ This man page was written by Tim Bielawa <tbielawa@redhat.com>.
COPYRIGHT
---------
-Copyright © 2012, Red Hat, Inc..
+Copyright © 2012-2014, Red Hat, Inc..
Juicer is released under the terms of the GPLv3+ License.
@@ -369,4 +337,4 @@ SEE ALSO
--------
*juicer-admin*(1), *juicer.conf*(5), *fnmatch*(3)
-The Juicer Homepage: <https://github.com/juicer/juicer/>
+*The Juicer Homepage* -- <https://github.com/juicer/juicer/>
diff --git a/docs/man/man5/juicer.conf.5 b/docs/man/man5/juicer.conf.5
index 8a85501..4c499ba 100644
--- a/docs/man/man5/juicer.conf.5
+++ b/docs/man/man5/juicer.conf.5
@@ -2,12 +2,12 @@
.\" Title: juicer.conf
.\" Author: :doctype:manpage
.\" Generator: DocBook XSL Stylesheets v1.78.1 <http://docbook.sf.net/>
-.\" Date: 02/14/2014
+.\" Date: 02/19/2014
.\" Manual: Pulp repos and release carts
.\" Source: Juicer 0.7.0
.\" Language: English
.\"
-.TH "JUICER\&.CONF" "5" "02/14/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
+.TH "JUICER\&.CONF" "5" "02/19/2014" "Juicer 0\&.7\&.0" "Pulp repos and release carts"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
@@ -265,14 +265,14 @@ section\&.
Juicer was written by GCA\-PC, Red Hat, Inc\&.\&. This man page was written by Tim Bielawa <tbielawa@redhat\&.com>\&.
.SH "COPYRIGHT"
.sp
-Copyright \(co 2012, Red Hat, Inc\&.\&.
+Copyright \(co 2012\-2014, Red Hat, Inc\&.\&.
.sp
Juicer is released under the terms of the GPLv3+ License\&.
.SH "SEE ALSO"
.sp
\fBjuicer\fR(1), \fBjuicer\-admin\fR(1)
.sp
-The Juicer Homepage: https://github\&.com/juicer/juicer/
+\fBThe Juicer Homepage\fR \(em https://github\&.com/juicer/juicer/
.SH "AUTHOR"
.PP
\fB:doctype:manpage\fR
diff --git a/docs/man/man5/juicer.conf.5.asciidoc.in b/docs/man/man5/juicer.conf.5.asciidoc.in
index 5c5b3cd..b378cd8 100644
--- a/docs/man/man5/juicer.conf.5.asciidoc.in
+++ b/docs/man/man5/juicer.conf.5.asciidoc.in
@@ -182,7 +182,7 @@ by Tim Bielawa <tbielawa@redhat.com>.
COPYRIGHT
---------
-Copyright © 2012, Red Hat, Inc..
+Copyright © 2012-2014, Red Hat, Inc..
Juicer is released under the terms of the GPLv3+ License.
@@ -191,4 +191,4 @@ SEE ALSO
--------
*juicer*(1), *juicer-admin*(1)
-The Juicer Homepage: <https://github.com/juicer/juicer/>
+*The Juicer Homepage* -- <https://github.com/juicer/juicer/>
diff --git a/docs/markdown/repo_syntax.md b/docs/markdown/repo_syntax.md
new file mode 100644
index 0000000..8ed19bf
--- /dev/null
+++ b/docs/markdown/repo_syntax.md
@@ -0,0 +1,101 @@
+# Repo File Syntax
+
+## Introduction
+
+Repositories can be automatically created from a **json** document which
+follows a specific syntax. This document describes that syntax.
+
+The overall structure of the json document reads like this:
+
+- The base datastructure of the document is a list
+- Each item in the list is a repository definition (repo def)
+- Each repo def is a hash, or dictionary
+- Each hash MUST have a `name` key, which is the repository name
+- Each hash MAY have zero or more of the following **optional** keys
+ - `checksum_type`
+ - `feed`
+ - `env`
+
+## Repository Definition Keys
+
+- `name`
+ - **Required** - Yes
+ - **Description** - The repository name. Must match the regular expression `([a-zA-Z0-9-_.]+)` (alphanumeric characters, `.`, `-`, and `_`)
+
+
+- `checksum_type`
+ - **Description** - The checksum type to use when generating meta-data
+ - **Required** - No
+ - **Default Value** - `sha256`
+ - **Choices:**
+ - sha
+ - sha256
+
+- `feed`
+ - **Description** - A feed url from which to synchronize yum repository packages
+ - **Required** - No
+ - **Default Value** - None
+
+- `env`
+ - **Description** - A list of environments this repository should exist in
+ - **Required** - No
+ - **Default Value** - By default repositories are created in all environments
+
+## Example Repository Definition File
+
+Here's a simple repo def with just two repositories defined:
+**repo01**, and **repo02**. **repo01** uses all the default values,
+while **repo02** sets `checksum_type` to `sha`. These repos would be
+created in all environments.
+
+ [
+ {
+ "name": "repo01"
+ },
+ {
+ "name": "repo02",
+ "checksum_type": "sha"
+ }
+ ]
+
+We could have also written it like this:
+
+ [{"name": "repo01"}, {"name": "repo02", "checksum_type": "sha"}]
+
+Or even like this:
+
+ [
+ {"name": "repo01"},
+ {"name": "repo02", "checksum_type": "sha"}
+ ]
+
+Here's an example with another repository definition. It exercises all
+of the optional keys, as well as demonstrates some more alternative
+syntax:
+
+
+ [
+ {"name": "repo01", "env": ["prod"]},
+ {"name": "repo02"},
+ {
+ "name": "fedora_mirror",
+ "feed": "http://download.fedoraproject.org/pub/fedora/linux/releases/20/Everything/x86_64/os/",
+ "checksum_type": "sha",
+ "env": ["dev", "prod"]
+ }
+ ]
+
+- **repo01** will only be created in *prod*
+- **repo02** is created in *all environments*
+
+- **fedora_mirror** syncs content from it's feed of
+ *http://download.fedoraproject.org/pub/fedora/linux/releases/20/Everything/x86_64/os/*,
+ sets it's checksum type to *sha*, and only is created in the *dev*
+ and *prod* environments
+
+# Protips (Troubleshooting)
+
+- Don't end lists or hashes with trailing commas
+- Remember to close all of your braces and brackets: Each `[` has a matching `]`, each `{` has a matching `}`
+- Use a `javascript` mode in your editor if it doesn't have a native `json` mode
+- Setting `env` to an empty list (`[]`) will **not** delete the repo from any environment
diff --git a/hacking/repo_def/01_repo_def_example_good.json b/hacking/repo_def/01_repo_def_example_good.json
new file mode 120000
index 0000000..4990664
--- /dev/null
+++ b/hacking/repo_def/01_repo_def_example_good.json
@@ -0,0 +1 @@
+../../share/juicer/repo_def_example.json
\ No newline at end of file
diff --git a/hacking/repo_def/02_env_not_list.json b/hacking/repo_def/02_env_not_list.json
new file mode 100644
index 0000000..1d32e71
--- /dev/null
+++ b/hacking/repo_def/02_env_not_list.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name": "repodef03",
+ "env": "prod"
+ }
+]
diff --git a/hacking/repo_def/03_extra_keys.json b/hacking/repo_def/03_extra_keys.json
new file mode 100644
index 0000000..368618e
--- /dev/null
+++ b/hacking/repo_def/03_extra_keys.json
@@ -0,0 +1,9 @@
+[
+ {
+ "name": "repodef03",
+ "checksum_type": "sha",
+ "feed": "http://foo",
+ "env": ["prod"],
+ "extra_key": "don't need this"
+ }
+]
diff --git a/hacking/repo_def/04_feed_not_string.json b/hacking/repo_def/04_feed_not_string.json
new file mode 100644
index 0000000..2ccbf42
--- /dev/null
+++ b/hacking/repo_def/04_feed_not_string.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name": "repodef03",
+ "feed": ["list", "of", "feeds"]
+ }
+]
diff --git a/hacking/repo_def/05_invalid_checksum_type.json b/hacking/repo_def/05_invalid_checksum_type.json
new file mode 100644
index 0000000..a6159e4
--- /dev/null
+++ b/hacking/repo_def/05_invalid_checksum_type.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name": "repodef03",
+ "checksum_type": "shu256"
+ }
+]
diff --git a/hacking/repo_def/06_invalid_json_document.json b/hacking/repo_def/06_invalid_json_document.json
new file mode 100644
index 0000000..022ef24
--- /dev/null
+++ b/hacking/repo_def/06_invalid_json_document.json
@@ -0,0 +1,5 @@
+&[
+ {
+ "name": "repodef03",
+ }
+]
diff --git a/hacking/repo_def/07_invalid_keys.json b/hacking/repo_def/07_invalid_keys.json
new file mode 100644
index 0000000..e366272
--- /dev/null
+++ b/hacking/repo_def/07_invalid_keys.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name": "repodef03",
+ "invalid_key": "what is this?"
+ }
+]
diff --git a/hacking/repo_def/08_invalid_repo_name.json b/hacking/repo_def/08_invalid_repo_name.json
new file mode 100644
index 0000000..3d8ba0c
--- /dev/null
+++ b/hacking/repo_def/08_invalid_repo_name.json
@@ -0,0 +1,5 @@
+[
+ {
+ "name": "$$r*ffpodef0@#3"
+ }
+]
diff --git a/hacking/repo_def/09_json_doc_not_list.json b/hacking/repo_def/09_json_doc_not_list.json
new file mode 100644
index 0000000..5ef923d
--- /dev/null
+++ b/hacking/repo_def/09_json_doc_not_list.json
@@ -0,0 +1,3 @@
+{
+ "name": "repodef03"
+}
diff --git a/hacking/repo_def/10_missing_required_keys.json b/hacking/repo_def/10_missing_required_keys.json
new file mode 100644
index 0000000..0dda711
--- /dev/null
+++ b/hacking/repo_def/10_missing_required_keys.json
@@ -0,0 +1,5 @@
+[
+ {
+ "badkey": "is bad"
+ }
+]
diff --git a/hacking/repo_def/11_repo_not_not_dict.json b/hacking/repo_def/11_repo_not_not_dict.json
new file mode 100644
index 0000000..6dc7bd4
--- /dev/null
+++ b/hacking/repo_def/11_repo_not_not_dict.json
@@ -0,0 +1,3 @@
+[
+ "repo01"
+]
diff --git a/hacking/setup-env b/hacking/setup-env
index e8b7032..2a8cfd9 100644
--- a/hacking/setup-env
+++ b/hacking/setup-env
@@ -1,6 +1,6 @@
# -*- mode: shell-script -*-
-PREFIX_PYTHONPATH="`pwd`:`pwd`/Juicer/:`pwd`/JuicerAdmin/"
+PREFIX_PYTHONPATH="`pwd`:`pwd`/Juicer/:`pwd`/JuicerAdmin/:`pwd`/hacking/"
PREFIX_PATH="`pwd`/bin"
PREFIX_MANPATH="`pwd`/docs/man"
diff --git a/hacking/tests b/hacking/tests
index f865279..dab86d1 100755
--- a/hacking/tests
+++ b/hacking/tests
@@ -1,7 +1,7 @@
#!/bin/bash
# -*- coding: utf-8 -*-
# Juicer - Administer Pulp and Release Carts
-# Copyright © 2012, Red Hat, Inc.
+# Copyright © 2012-2014, 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
@@ -16,7 +16,51 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-cd juicer/tests/
+######################################################################
+# Catch a ^C exit so the for loops don't keep running
+######################################################################
+trap "{ exit 0; }" SIGINT SIGTERM
+######################################################################
+
+. ./hacking/setup-env
+
+# Oh SWEET. Color codes! Ganked from http://stackoverflow.com/a/10466960/263969
+RESTORE='\033[0m'
+RED='\033[00;31m'
+GREEN='\033[00;32m'
+
+green() {
+ echo -en "${RESTORE}${GREEN}${1}${RESTORE}"
+}
+
+red() {
+ echo -ne "${RESTORE}${RED}${1}${RESTORE}"
+}
+
+######################################################################
+
+echo "#################################"
+echo "Running repo def validation tests"
+echo "#################################"
+
+# Count how many tests we're running
+NUM_TESTS=`ls -1 ./hacking/repo_def/*.json | wc -l`
+# Only one of them should pass
+MUST_FAIL=$(( ${NUM_TESTS} - 1 ))
+echo "Expecting `green '1 PASS'` and `red \"${MUST_FAIL} FAIL\"`s"
+
+python ./juicer/utils/ValidateRepoDef.py ./hacking/repo_def/*.json
+if [ "$?" == "${MUST_FAIL}" ]; then
+ echo "Repo def validation tests success: (`green '1 PASS'`/`red \"${MUST_FAIL} FAIL\"`s)"
+else
+ echo `red "Repo def validation failed"`
+fi
+
+######################################################################
+# Run the rest of the tests
+######################################################################
+
+pushd juicer/tests/
for test in test_hello test_pull test_show test_workflow;
do
@@ -30,4 +74,4 @@ do
python -m unittest TestJuicerAdmin.TestJuicerAdmin.$test
done
-cd -
+popd
diff --git a/juicer/__init__.py b/juicer/__init__.py
index fe78243..41ab564 100644
--- a/juicer/__init__.py
+++ b/juicer/__init__.py
@@ -17,4 +17,4 @@
# Juicer package
-__version__ = '0.7.0-3a4a3cc'
+__version__ = '0.7.0-5e23949'
diff --git a/juicer/admin/JuicerAdmin.py b/juicer/admin/JuicerAdmin.py
index c806180..4103d35 100644
--- a/juicer/admin/JuicerAdmin.py
+++ b/juicer/admin/JuicerAdmin.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Juicer - Administer Pulp and Release Carts
-# Copyright © 2012,2013, Red Hat, Inc.
+# Copyright © 2012-2014, 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
@@ -17,10 +17,13 @@
from juicer.common import Constants
from juicer.common.Errors import *
+from juicer.common.Repo import JuicerRepo, PulpRepo
import juicer.admin
import juicer.utils
import juicer.utils.Log
+import juicer.utils.ValidateRepoDef
import re
+import json
class JuicerAdmin(object):
@@ -43,6 +46,8 @@ class JuicerAdmin(object):
`repo_name` - Name of repository to create
`feed` - Repo URL to feed from
`checksum_type` - Used for generating meta-data
+ `from_file` - JSON file of repo definitions
+ `noop` - Boolean, if true don't actually create/update repos, just show what would have happened
Create repository in specified environments, associate the
yum_distributor with it and publish the repo
@@ -108,6 +113,87 @@ class JuicerAdmin(object):
_r.raise_for_status()
return True
+ def import_repo(self, from_file, noop=False, query='/repositories/'):
+ """
+ `from_file` - JSON file of repo definitions
+ `noop` - Boolean, if true don't actually create/update repos, just show what would have happened
+ """
+ try:
+ repo_defs = juicer.utils.ValidateRepoDef.validate_document(from_file)
+ except JuicerRepoDefError, e:
+ juicer.utils.Log.log_error("Could not load repo defs from %s:" % from_file)
+ raise e
+ else:
+ juicer.utils.Log.log_debug("Loaded and validated repo defs from %s" % from_file)
+
+ # All our known envs, for cases where no value is supplied for 'env'
+ all_envs = juicer.utils.get_environments()
+
+ # Repos to create/update, sorted by environment.
+ repo_objects_create = []
+ repo_objects_update = {}
+ for env in all_envs:
+ repo_objects_update[env] = []
+
+ # All repo defs as Repo objects
+ all_repos = [JuicerRepo(repo['name'], repo_def=repo) for repo in repo_defs]
+
+ # Detailed information on all existing repos
+ #
+ # TODO: Optimize this so we only call to pulp for environments
+ # we KNOW we need to operate in. Determine this by evaluating
+ # the 'env' property of each repo def.
+ juicer.utils.Log.log_info("Loading information on all existing repos (this could take a while)")
+ existing_repos = self.list_repos(envs=all_envs)
+
+ # Use a cache to speed up testing
+ # juicer.utils.Log.log_info("BE AWARE: Currently reading repo list from local cache")
+ # existing_repos = juicer.utils.read_json_document('/tmp/repo_list.json')
+
+ for repo in all_repos:
+ # 'env' is all environments if: 'env' is not defined; 'env' is an empty list
+ current_env = repo.get('env', [])
+ if current_env == []:
+ juicer.utils.Log.log_debug("Setting 'env' to all_envs for repo: %s" % repo['name'])
+ repo['env'] = all_envs
+
+ # Assemble a set of all specified environments.
+ defined_envs = juicer.utils.unique_repo_def_envs(all_repos)
+ juicer.utils.Log.log_notice("Discovered environments: %s" % ", ".join(list(defined_envs)))
+
+ # sort out new vs. existing
+ for repo in all_repos:
+ # Does the repo refer to environments in our juicer.conf file?
+ if juicer.utils.repo_in_defined_envs(repo, all_envs):
+ repo['reality_check_in_env'] = []
+ repo['missing_in_env'] = []
+ for env in repo['env']:
+ if juicer.utils.repo_exists_in_repo_list(repo, existing_repos[env]):
+ # Does the repo def match what exists already?
+ pulp_repo = self.show_repo(repo_names=[repo['name']], envs=[env])
+ #juicer.utils.Log.log_debug(str(pulp_repo))
+ repo_diff = juicer.utils.repo_def_matches_reality(repo, pulp_repo[env][0])
+ if not repo_diff.diff()['distributor'] or repo_diff.diff()['importer']:
+ juicer.utils.Log.log_notice("Repo %s already exists, but reality does not the definition", repo['name'])
+ repo['reality_check_in_env'].append((env, repo_diff))
+ else:
+ juicer.utils.Log.log_notice("Repo %s already exists and is correct", repo['name'])
+ else:
+ # The repo does not exist yet in reality
+ juicer.utils.Log.log_notice("Need to create %s in %s", repo['name'], env)
+ repo['missing_in_env'].append(env)
+
+ # Do we need to create the repo anywhere?
+ if repo['missing_in_env']:
+ repo_objects_create.append(repo)
+
+ # We we need to update the repo anywhere?
+ if repo['reality_check_in_env']:
+ for env,diff in repo['reality_check_in_env']:
+ repo_objects_update[env].append(repo)
+
+ return (repo_objects_create, repo_objects_update)
+
def create_user(self, login=None, password=None, user_name=None, envs=[], query='/users/'):
"""
`login` - Login or username for user
@@ -236,23 +322,19 @@ class JuicerAdmin(object):
juicer.utils.Log.log_debug(
"List Repos In: %s", ", ".join(envs))
- count = 0
-
+ repo_lists = {}
for env in envs:
- count += 1
+ repo_lists[env] = []
- juicer.utils.Log.log_info("%s:", env)
+ for env in envs:
_r = self.connectors[env].get(query)
if _r.status_code == Constants.PULP_GET_OK:
for repo in juicer.utils.load_json_str(_r.content):
if re.match(".*-{0}$".format(env), repo['id']):
- juicer.utils.Log.log_info(repo['display_name'])
-
- if count < len(envs):
- juicer.utils.Log.log_info("")
+ repo_lists[env].append(repo['display_name'])
else:
_r.raise_for_status()
- return True
+ return repo_lists
def list_users(self, envs=[], query="/users/"):
"""
@@ -304,41 +386,37 @@ class JuicerAdmin(object):
_r.raise_for_status()
return True
- def show_repo(self, repo_name=None, envs=[], query='/repositories/'):
+ def show_repo(self, repo_names=[], envs=[], query='/repositories/'):
"""
- `repo_name` - Name of repository to show
+ `repo_names` - Name of repository(s) to show
Show repositories in specified environments
"""
- juicer.utils.Log.log_debug("Show Repo: %s", repo_name)
-
- # keep track of which iteration of environment we're in
- count = 0
+ juicer.utils.Log.log_debug("Show Repo(s): %s", str(repo_names))
+ repo_objects = {}
for env in envs:
- count += 1
-
- juicer.utils.Log.log_info("%s:", env)
- url = "%s%s-%s/" % (query, repo_name, env)
- _r = self.connectors[env].get(url)
- if _r.status_code == Constants.PULP_GET_OK:
- repo = juicer.utils.load_json_str(_r.content)
+ repo_objects[env] = []
- juicer.utils.Log.log_info(repo['display_name'])
- try:
- juicer.utils.Log.log_info("%s packages" % repo['content_unit_counts']['rpm'])
- except:
- juicer.utils.Log.log_info("0 packages")
-
- if count < len(envs):
- # just want a new line
- juicer.utils.Log.log_info("")
- else:
- if _r.status_code == Constants.PULP_GET_NOT_FOUND:
- raise JuicerPulpError("repo '%s' was not found" % repo_name)
+ for env in envs:
+ juicer.utils.Log.log_debug("scanning environment: %s", env)
+ for repo_name in repo_names:
+ juicer.utils.Log.log_debug("looking for repo: %s", repo_name)
+ url = "%s%s-%s/?details=true" % (query, repo_name, env)
+ _r = self.connectors[env].get(url)
+ if _r.status_code == Constants.PULP_GET_OK:
+ juicer.utils.Log.log_debug("found repo: %s", repo_name)
+ repo = juicer.utils.load_json_str(_r.content)
+ repo_object = PulpRepo(repo_name, env, repo_def=repo)
+ repo_objects[env].append(repo_object)
else:
- _r.raise_for_status()
- return True
+ if _r.status_code == Constants.PULP_GET_NOT_FOUND:
+ juicer.utils.Log.log_warn("could not find repo '%s' in %s" % (repo_name, env))
+ else:
+ _r.raise_for_status()
+ for k,v in repo_objects.iteritems():
+ juicer.utils.Log.log_debug("environment %s: found %d repos" % (k, len(v)))
+ return repo_objects
def show_user(self, login=None, envs=[], query='/users/'):
"""
diff --git a/juicer/admin/Parser.py b/juicer/admin/Parser.py
index 190f9fb..ed43e44 100644
--- a/juicer/admin/Parser.py
+++ b/juicer/admin/Parser.py
@@ -28,271 +28,305 @@ class Parser(object):
self._default_envs = juicer.utils.get_environments()
- self.parser.add_argument('-v', action='count', \
- default=1, \
+ self.parser.add_argument('-v', action='count',
+ default=1,
help='Increase the verbosity (up to 3x)')
- self.parser.add_argument('-V', '--version', action='version', \
- version="juicer-admin-%s" \
- % juicer.utils.juicer_version())
+ self.parser.add_argument('-V', '--version', action='version',
+ version="juicer-admin-%s"
+ % juicer.utils.juicer_version())
##################################################################
# Keep the different commands separate
- self.subparsers = self.parser.add_subparsers(title='Commands', \
- dest='command', \
- description='\'%(prog)s COMMAND -h\' for individual help topics')
+ self.subparsers = self.parser.add_subparsers(title='Commands',
+ dest='command',
+ description='\'%(prog)s COMMAND -h\' for individual help topics')
##################################################################
# Create the 'repo' sub-parser
- parser_repo = self.subparsers.add_parser('repo', \
- help='Repo operations')
+ parser_repo = self.subparsers.add_parser('repo',
+ help='Repo operations')
subparser_repo = parser_repo.add_subparsers(dest='sub_command')
##################################################################
# Create the 'user' sub-parser
- parser_user = self.subparsers.add_parser('user', \
- help='User operations')
+ parser_user = self.subparsers.add_parser('user',
+ help='User operations')
subparser_user = parser_user.add_subparsers(dest='sub_command')
##################################################################
# Create the 'role' sub-parser
- parser_role = self.subparsers.add_parser('role', \
- help='Role operations')
+ parser_role = self.subparsers.add_parser('role',
+ help='Role operations')
subparser_role = parser_role.add_subparsers(dest='sub_command')
##################################################################
# Create the 'repo create' sub-parser
- parser_repo_create = subparser_repo.add_parser('create',\
- help='Create pulp repository', \
- usage='%(prog)s REPONAME --arch ARCH --feed FEED --checksum-type CHECKSUM-TYPE --in [ENV ...]')
+ parser_repo_create = subparser_repo.add_parser('create',
+ help='Create pulp repository',
+ usage='%(prog)s REPONAME { [--arch ARCH] [--feed FEED] [--checksum-type CHECKSUM-TYPE] | --from-file JSON_DEFS} [--in ENV [...]]')
- parser_repo_create.add_argument('name', metavar='name', \
- help='The name of your repo')
+ parser_repo_create.add_argument('name', metavar='name',
+ help='The name of your repo')
- parser_repo_create.add_argument('--arch', metavar='arch', \
- default='noarch', \
+ parser_repo_create.add_argument('--arch', metavar='arch',
+ default='noarch',
help='The architecture of your repo (default: noarch)')
- parser_repo_create.add_argument('--feed', metavar='feed', \
- default=None, \
- help='A feed repo for your repo')
+ parser_repo_create.add_argument('--feed', metavar='feed',
+ default=None,
+ help='A feed repo for your repo')
- parser_repo_create.add_argument('--checksum-type', metavar='checksum_type', \
- default='sha256', \
- choices=['sha26', 'sha'], \
- help='Checksum-type used for meta-data generation (one of: sha26, sha)')
+ parser_repo_create.add_argument('--checksum-type', metavar='checksum_type',
+ default='sha256',
+ choices=['sha26', 'sha'],
+ help='Checksum-type used for meta-data generation (one of: sha26, sha)')
- parser_repo_create.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to create your repository')
+ parser_repo_create.add_argument('--noop', '--dry-run', '-n',
+ default=False, action='store_true',
+ help="Don't create the repos, just show what would have happened")
+
+ parser_repo_create.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to create your repository')
parser_repo_create.set_defaults(ja=juicer.admin.create_repo)
##################################################################
+ # Create the 'repo import' sub-parser
+ import_description = """This will create repositories matching the definitions in the repo
+def file. Repositories which already exist will be updated. See the
+"repo import" and "Repo Def Format" sections in juicer-admin(1) for
+instructions on how to write a proper repo def file."""
+
+ parser_repo_import = subparser_repo.add_parser('import',
+ help='Create pulp repositories from an imported definition',
+ usage='%(prog)s --from-file JSON_DEFS [--noop]',
+ description=import_description)
+
+ parser_repo_import.add_argument('--from-file', metavar='json_defs', default=None,
+ help='Repository definition file in JSON format')
+
+ parser_repo_import.add_argument('--noop', '--dry-run', '-n',
+ default=False, action='store_true',
+ help="Don't create the repos, just show what would have happened")
+
+ parser_repo_import.set_defaults(ja=juicer.admin.import_repo)
+
+ ##################################################################
# Create the 'user create' sub-parser
- parser_user_create = subparser_user.add_parser('create',\
- help='Create pulp user', \
- usage='%(prog)s LOGIN --name FULLNAME --password PASSWORD \
- \n\nYou will be prompted if PASSWORD argument not supplied.')
-
- parser_user_create.add_argument('login', metavar='login', \
- help='Login user id for user')
-
- parser_user_create.add_argument('--name', metavar='name', \
- dest='name', \
- required=True, \
- help='Full name of user')
-
- parser_user_create.add_argument('--password', metavar='password', \
- dest='password', \
- nargs='*', \
- required=True, \
- action=PromptAction, \
+ parser_user_create = subparser_user.add_parser('create',
+ help='Create pulp user',
+ usage='%(prog)s LOGIN --name FULLNAME --password PASSWORD \
+ \n\nYou will be prompted if the PASSWORD argument not supplied.')
+
+ parser_user_create.add_argument('login', metavar='login',
+ help='Login user id for user')
+
+ parser_user_create.add_argument('--name', metavar='name',
+ dest='name',
+ required=True,
+ help='Full name of user')
+
+ parser_user_create.add_argument('--password', metavar='password',
+ dest='password',
+ nargs='*',
+ required=True,
+ action=PromptAction,
help='Plain text password for user')
- parser_user_create.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to create pulp user')
+ parser_user_create.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to create pulp user')
parser_user_create.set_defaults(ja=juicer.admin.create_user)
##################################################################
# Create the 'user update' sub-parser
- parser_user_update = subparser_user.add_parser('update',\
- help='Change user information', \
- usage='%(prog)s LOGIN --name FULLNAME --password PASSWORD \
- \n\nYou will be prompted if PASSWORD argument not supplied.')
-
- parser_user_update.add_argument('login', metavar='login', \
- help='Login user id for user to update')
-
- parser_user_update.add_argument('--name', metavar='name', \
- dest='name', \
- required=False, \
- help='Updated name of user')
-
- parser_user_update.add_argument('--password', metavar='password', \
- dest='password', \
- nargs='*', \
- required=False, \
- action=PromptAction, \
+ parser_user_update = subparser_user.add_parser('update',
+ help='Change user information',
+ usage='%(prog)s LOGIN --name FULLNAME --password PASSWORD \
+ \n\nYou will be prompted if the PASSWORD argument not supplied.')
+
+ parser_user_update.add_argument('login', metavar='login',
+ help='Login user id for user to update')
+
+ parser_user_update.add_argument('--name', metavar='name',
+ dest='name',
+ required=False,
+ help='Updated name of user')
+
+ parser_user_update.add_argument('--password', metavar='password',
+ dest='password',
+ nargs='*',
+ required=False,
+ action=PromptAction,
help='Updated password for user')
- parser_user_update.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to create pulp user')
+ parser_user_update.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to create pulp user')
parser_user_update.set_defaults(ja=juicer.admin.update_user)
##################################################################
# Create the 'repo list' sub-parser
- parser_repo_list = subparser_repo.add_parser('list', \
- help='List all repos')
+ parser_repo_list = subparser_repo.add_parser('list',
+ help='List all repos')
- parser_repo_list.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to list repos')
+ parser_repo_list.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to list repos')
+
+ parser_repo_list.add_argument('--json',
+ action='store_true', default=False,
+ help='Dump everything in JSON format')
parser_repo_list.set_defaults(ja=juicer.admin.list_repos)
##################################################################
# Create the 'user list' sub-parser
- parser_user_list = subparser_user.add_parser('list', \
- help='List all users')
+ parser_user_list = subparser_user.add_parser('list',
+ help='List all users')
- parser_user_list.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to list users')
+ parser_user_list.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to list users')
parser_user_list.set_defaults(ja=juicer.admin.list_users)
##################################################################
# Create the 'repo sync' sub-parser
- parser_repo_sync = subparser_repo.add_parser('sync', \
- help='Sync pulp repository')
+ parser_repo_sync = subparser_repo.add_parser('sync',
+ help='Sync pulp repository')
- parser_repo_sync.add_argument('name', metavar='name', \
- help='The name of your repo')
+ parser_repo_sync.add_argument('name', metavar='name',
+ help='The name of your repo')
- parser_repo_sync.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to sync your repository')
+ parser_repo_sync.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to sync your repository')
parser_repo_sync.set_defaults(ja=juicer.admin.sync_repo)
##################################################################
# Create the 'repo show' sub-parser
- parser_repo_show = subparser_repo.add_parser('show', \
- usage='%(prog)s name --in [ENV ...]', \
- help='Show pulp repository')
+ parser_repo_show = subparser_repo.add_parser('show',
+ usage='%(prog)s name [...] [--json] --in [ENV ...]',
+ help='Show pulp repository(s)')
+
+ parser_repo_show.add_argument('name', metavar='name',
+ nargs="+",
+ help='The name of your repo(s)')
- parser_repo_show.add_argument('name', metavar='name', \
- help='The name of your repo')
+ parser_repo_show.add_argument('--json',
+ action='store_true', default=False,
+ help='Dump everything in JSON format')
- parser_repo_show.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to show your repository')
+ parser_repo_show.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to show your repository')
parser_repo_show.set_defaults(ja=juicer.admin.show_repo)
##################################################################
# Create the 'user show' sub-parser
- parser_user_show = subparser_user.add_parser('show', \
- usage='%(prog)s LOGIN --in [ENV ...]', \
- help='Show pulp user')
+ parser_user_show = subparser_user.add_parser('show',
+ usage='%(prog)s LOGIN --in [ENV ...]',
+ help='Show pulp user')
- parser_user_show.add_argument('login', metavar='login', \
- help='Login user id for user')
+ parser_user_show.add_argument('login', metavar='login',
+ help='Login user id for user')
- parser_user_show.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to show user')
+ parser_user_show.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to show user')
parser_user_show.set_defaults(ja=juicer.admin.show_user)
##################################################################
# Create the 'repo delete' sub-parser
- parser_repo_delete = subparser_repo.add_parser('delete', \
- help='Delete pulp repository')
+ parser_repo_delete = subparser_repo.add_parser('delete',
+ help='Delete pulp repository')
- parser_repo_delete.add_argument('name', metavar='name', \
- help='The name of your repo')
+ parser_repo_delete.add_argument('name', metavar='name',
+ help='The name of your repo')
- parser_repo_delete.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to delete your repository')
+ parser_repo_delete.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to delete your repository')
parser_repo_delete.set_defaults(ja=juicer.admin.delete_repo)
##################################################################
# Create the 'user delete' sub-parser
- parser_user_delete = subparser_user.add_parser('delete', \
- help='Delete pulp user')
+ parser_user_delete = subparser_user.add_parser('delete',
+ help='Delete pulp user')
- parser_user_delete.add_argument('login', metavar='login', \
- help='Login user id for user')
+ parser_user_delete.add_argument('login', metavar='login',
+ help='Login user id for user')
- parser_user_delete.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to delete user')
+ parser_user_delete.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to delete user')
parser_user_delete.set_defaults(ja=juicer.admin.delete_user)
##################################################################
# Create the 'role add' sub-parser
- parser_role_add = subparser_role.add_parser('add', \
- help='Add user to role')
+ parser_role_add = subparser_role.add_parser('add',
+ help='Add user to role')
- parser_role_add.add_argument('--login', metavar='login', \
- help='Login user id for user', \
- required=True)
+ parser_role_add.add_argument('--login', metavar='login',
+ help='Login user id for user',
+ required=True)
- parser_role_add.add_argument('--role', metavar='role', \
- help='Role to add user to', \
- required=True)
+ parser_role_add.add_argument('--role', metavar='role',
+ help='Role to add user to',
+ required=True)
- parser_role_add.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to add user to role')
+ parser_role_add.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to add user to role')
parser_role_add.set_defaults(ja=juicer.admin.role_add)
##################################################################
# Create the 'role list' sub-parser
- parser_role_list = subparser_role.add_parser('list', \
- help='List all roles')
-
- parser_role_list.add_argument('--in', metavar='envs', \
- nargs="+", \
- dest='envs', \
- default=self._default_envs, \
- help='The environments in which to list roles')
+ parser_role_list = subparser_role.add_parser('list',
+ help='List all roles')
+
+ parser_role_list.add_argument('--in', metavar='envs',
+ nargs="+",
+ dest='envs',
+ default=self._default_envs,
+ help='The environments in which to list roles')
parser_role_list.set_defaults(ja=juicer.admin.list_roles)
diff --git a/juicer/admin/__init__.py b/juicer/admin/__init__.py
index 88c5d35..4127d46 100644
--- a/juicer/admin/__init__.py
+++ b/juicer/admin/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Juicer - Administer Pulp and Release Carts
-# Copyright © 2012,2013, Red Hat, Inc.
+# Copyright © 2012-2014, 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
@@ -16,22 +16,48 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from juicer.admin.JuicerAdmin import JuicerAdmin as ja
-
+import juicer.utils
def create_repo(args):
pulp = ja(args)
pulp.create_repo(args.arch, args.name, args.feed, args.envs, args.checksum_type)
+def import_repo(args):
+ pulp = ja(args)
+ # Get our TODO specs from juicer-admin
+ (to_create, to_update) = pulp.import_repo(args.from_file, args.noop)
+
+ if args.noop:
+ juicer.utils.Log.log_info("NOOP: Would have created repos with definitions:")
+ juicer.utils.Log.log_info("%s", juicer.utils.create_json_str(to_create, indent=4, cls=juicer.common.Repo.RepoEncoder))
+
+ juicer.utils.Log.log_info("NOOP: Would have updated repos with definitions:")
+ juicer.utils.Log.log_info("%s", juicer.utils.create_json_str(to_update, indent=4, cls=juicer.common.Repo.RepoEncoder))
+ else:
+ for repo in to_create:
+ pulp.create_repo(repo_name=repo['name'],
+ feed=repo['feed'],
+ envs=repo['missing_in_env'],
+ checksum_type=repo['checksum_type'])
+
+ for env,repos in to_update.iteritems():
+ for repo in repos:
+ juicer.utils.Log.log_info("Would have updated %s-%s", repo, env)
+
+
def create_user(args):
pulp = ja(args)
pulp.create_user(args.login, args.password, args.name, args.envs)
-
def list_repos(args):
pulp = ja(args)
- pulp.list_repos(args.envs)
-
+ repo_lists = pulp.list_repos(args.envs)
+ if args.json:
+ print juicer.utils.create_json_str(repo_lists, indent=4)
+ else:
+ for env, repos in repo_lists.iteritems():
+ print "%s(%d): %s" % (env, len(repos), ' '.join(repos))
def sync_repo(args):
pulp = ja(args)
@@ -40,8 +66,33 @@ def sync_repo(args):
def show_repo(args):
pulp = ja(args)
- pulp.show_repo(args.name, args.envs)
-
+ repo_objects = pulp.show_repo(args.name, args.envs)
+
+ if args.json:
+ # JSON output requested
+ print juicer.utils.create_json_str(repo_objects, indent=4,
+ cls=juicer.common.Repo.RepoEncoder)
+ else:
+ found_repos = 0
+ for env, repos in repo_objects.iteritems():
+ found_repos += len(repos)
+ if found_repos == 0:
+ print "Could not locate repo(s) in any environment"
+ return False
+
+ # Human readable table-style output by default
+ rows = [['Repo', 'Env', 'RPMs', 'SRPMs', 'Checksum']]
+ for env,repos in repo_objects.iteritems():
+ # 'repos' contains a list of hashes
+ for repo in repos:
+ # each hash represents a repo
+ repo_name = repo['name']
+ repo_rpm_count = repo['rpm_count']
+ repo_srpm_count = repo['srpm_count']
+ repo_checksum = repo['checksum']
+ rows.append([repo_name, env, repo_rpm_count, repo_srpm_count, repo_checksum])
+
+ print juicer.utils.table(rows)
def show_user(args):
pulp = ja(args)
diff --git a/juicer/common/Constants.py b/juicer/common/Constants.py
index 9b25196..4155247 100644
--- a/juicer/common/Constants.py
+++ b/juicer/common/Constants.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Juicer - Administer Pulp and Release Carts
-# Copyright © 2012, Red Hat, Inc.
+# Copyright © 2012-2014, 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
@@ -17,7 +17,6 @@
import os.path
-
######################################################################
# PULP RETURN CODES
######################################################################
@@ -75,3 +74,16 @@ EXAMPLE_SYSTEM_CONFIG = '/usr/share/juicer/juicer.conf'
######################################################################
# The version the server should be running
EXPECTED_SERVER_VERSION = '2.3'
+
+######################################################################
+# Repo def file defaults/attributes
+REPO_DEF_DEFAULTS = {
+ 'name': None,
+ 'feed': None,
+ 'env': [],
+ 'checksum_type': 'sha256',
+}
+
+REPO_DEF_REQ_KEYS = ['name']
+REPO_DEF_OPT_KEYS = ['checksum_type', 'feed', 'env']
+REPO_DEF_CHECKSUM_TYPES = ['sha', 'sha256']
diff --git a/juicer/common/Errors.py b/juicer/common/Errors.py
index f254eb0..b3ba10f 100644
--- a/juicer/common/Errors.py
+++ b/juicer/common/Errors.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Juicer - Administer Pulp and Release Carts
-# Copyright © 2012, Red Hat, Inc.
+# Copyright © 2012-2014, 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
@@ -46,3 +46,15 @@ class JuicerManifestError(JuicerError):
class JuicerRpmSignPluginError(JuicerError):
pass
+
+class JuicerRepoExclusionError(JuicerError):
+ pass
+
+class JuicerRepoDefError(JuicerError):
+ """
+ Raised for invalid repo def files
+ """
+ pass
+
+class JuicerRepoInUndefinedEnvs(JuicerError):
+ pass
diff --git a/juicer/common/Repo.py b/juicer/common/Repo.py
new file mode 100644
index 0000000..019e153
--- /dev/null
+++ b/juicer/common/Repo.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Juicer - Administer Pulp and Release Carts
+# Copyright © 2014, 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 3 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 json
+import juicer.utils.Log
+import juicer.utils
+
+
+class Repo(object):
+ """
+ Internal representation of a repository object
+ """
+ def __init__(self, repo_name, env=None, repo_def=None):
+ """
+ `repo_name` - Name of this repo
+ `env` - Environment the repo lives in
+ `repo_def` - Repository definition as per juicer (see: docs/markdown/repo_syntax.md)
+ """
+ juicer.utils.Log.log_debug("creating repo object for %s-%s" % (repo_name, env))
+ self.spec = {}
+ self['env'] = env
+ self._parse_repo_def(repo_def)
+ juicer.utils.Log.log_debug("instantiated Repo object for %s-%s" % (repo_name, env))
+
+ def _parse_repo_def(self, repo_def):
+ raise NotImplementedError("Don't call the Repo object directly. Instead call a PulpRepo or JuicerRepo")
+
+ def __setitem__(self, key, value):
+ self.spec[key] = value
+
+ def __getitem__(self, item):
+ if item in self.spec:
+ return self.spec[item]
+ else:
+ raise KeyError(item)
+
+ def __contains__(self, item):
+ return item in self.spec
+
+ def __str__(self):
+ return juicer.utils.create_json_str(self.spec)
+
+ def _repo_ds(self):
+ return self.spec
+
+ def get(self, key, default=None):
+ if key in self.spec:
+ return self.spec[key]
+ else:
+ return default
+
+
+class JuicerRepo(Repo):
+ """
+ Internal representation of a Juicer repository object
+ """
+ def _parse_repo_def(self, repo_def):
+ juicer.utils.Log.log_debug("parsing juicer definition")
+ self['name'] = repo_def['name']
+ defaults = juicer.common.Constants.REPO_DEF_DEFAULTS
+ for key in juicer.common.Constants.REPO_DEF_OPT_KEYS:
+ self[key] = repo_def.get(key, defaults[key])
+ juicer.utils.Log.log_debug("Defined %s as %s" % (key, str(self[key])))
+ juicer.utils.Log.log_debug("finished parsing juicer definition")
+
+
+class PulpRepo(Repo):
+ """
+ Internal representation of a Pulp repository object
+ """
+ def _parse_repo_def(self, repo_def):
+ juicer.utils.Log.log_debug("parsing pulp definition")
+ #juicer.utils.Log.log_debug(juicer.utils.create_json_str(repo_def, indent=4))
+ self.spec = repo_def
+ self['name'] = repo_def['display_name']
+ self['rpm_count'] = repo_def.get('content_unit_counts', {}).get('rpm', 0)
+ self['srpm_count'] = repo_def.get('content_unit_counts', {}).get('srpm', 0)
+ # There's no pretty way to write this that doesn't take up 10 lines of code...
+ # Grab the deeply nested key 'checksum_type', or return 'sha256' if it doesn't exist
+ self['checksum_type'] = repo_def.get('distributors', [{}])[0].get('config', {}).get('checksum_type', 'sha256')
+
+ # Does this thing even have an importer defined?
+ self['feed'] = None
+ if 'importers' in self.spec:
+ # Assume we were intelligent any only defined one importer
+ importer = self.spec['importers'][0]
+ if 'config' in importer:
+ # Good, it is configured
+ if 'feed' in importer['config']:
+ # OK, it has a feed set, track it
+ self.spec['feed'] = importer['config']['feed']
+ juicer.utils.Log.log_debug("finished parsing pulp definition")
+
+
+class RepoDiff(object):
+ """Calculate the difference of a juicer repo and a pulp repo."""
+ def __init__(self, juicer_repo=None, pulp_repo=None):
+ if not type(juicer_repo) == juicer.common.Repo.JuicerRepo:
+ raise TypeError("juicer_repo option to RepoDiff is not a JuicerRepo")
+
+ if not type(pulp_repo) == juicer.common.Repo.PulpRepo:
+ raise TypeError("pulp_repo option to RepoDiff is not a PulpRepo")
+
+ self.j = juicer_repo
+ self.p = pulp_repo
+ self.distributor_diff = {}
+ self.distributor_diff['distributor_config'] = {}
+ self.importer_diff = {}
+ self.importer_diff['importer_config'] = {}
+ self._diff()
+
+ def diff(self):
+ """Return importer/distributor diff specs"""
+ return {
+ 'distributor': self.distributor_diff,
+ 'importer': self.importer_diff
+ }
+
+ def __str__(self):
+ return str({
+ 'distributor': self.distributor_diff,
+ 'importer': self.importer_diff
+ })
+
+ def _diff(self):
+ """Calculates what you need to do to make a pulp repo match a juicer repo def"""
+ j_cs = self.j['checksum_type']
+ j_feed = self.j['feed']
+
+ p_cs = self.p['checksum_type']
+ p_feed = self.p['feed']
+
+ # checksum is a distributor property
+ # Is the pulp checksum wrong?
+ if not p_cs == j_cs:
+ juicer.utils.Log.log_debug("Pulp checksum_type does not match juicer")
+ self.distributor_diff['distributor_config']['checksum_type'] = j_cs
+ juicer.utils.Log.log_debug("distributor_config::checksum_type SHOULD BE %s" % j_cs)
+
+ # feed is an importer property
+ if not p_feed == j_feed:
+ juicer.utils.Log.log_debug("Pulp feed does not match juicer")
+ self.importer_diff['importer_config']['feed'] = j_feed
+ juicer.utils.Log.log_debug("importer_config::feed SHOULD BE %s" % j_feed)
+
+
+# Custom encoder for Repo types so we can dump them with standard json tools
+class RepoEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, Repo):
+ return obj._repo_ds()
+ elif isinstance(obj, RepoDiff):
+ return str(obj)
+ # Let the base class default method raise the TypeError
+ return json.JSONEncoder.default(self, obj)
+
+# Custom encoder for RepoDiff types so we can dump them with standard json tools
+# class RepoDiffEncoder(json.JSONEncoder):
+# def default(self, obj):
+# if isinstance(obj, RepoDiff):
+# return str(obj)
+# # Let the base class default method raise the TypeError
+# return json.JSONEncoder.default(self, obj)
diff --git a/juicer/juicer/Parser.py b/juicer/juicer/Parser.py
index 105498d..89ec4d8 100644
--- a/juicer/juicer/Parser.py
+++ b/juicer/juicer/Parser.py
@@ -23,291 +23,291 @@ import juicer.utils
class Parser(object):
def __init__(self):
- self.parser = argparse.ArgumentParser(\
- description='Manage release carts')
+ self.parser = argparse.ArgumentParser(
+ description='Manage release carts')
juicer.juicer.parser = self.parser
self._default_start_in = juicer.utils.get_login_info()[1]['start_in']
self._default_envs = juicer.utils.get_environments()
- self.parser.add_argument('-v', action='count', \
- default=1, \
- help='Increase the verbosity (up to 3x)')
+ self.parser.add_argument('-v', action='count',
+ default=1,
+ help='Increase the verbosity (up to 3x)')
- self.parser.add_argument('-V', '--version', action='version', \
- version="juicer-%s" \
- % juicer.utils.juicer_version())
+ self.parser.add_argument('-V', '--version', action='version',
+ version="juicer-%s"
+ % juicer.utils.juicer_version())
##################################################################
# Keep the different commands separate
- subparsers = self.parser.add_subparsers(title='Commands', \
- dest='command', \
- description='\'%(prog)s COMMAND -h\' for individual help topics')
+ subparsers = self.parser.add_subparsers(title='Commands',
+ dest='command',
+ description='\'%(prog)s COMMAND -h\' for individual help topics')
##################################################################
# Create the 'cart' sub-parser
- parser_cart = subparsers.add_parser('cart', \
- help='Cart operations')
+ parser_cart = subparsers.add_parser('cart',
+ help='Cart operations')
subparser_cart = parser_cart.add_subparsers(dest='sub_command')
##################################################################
# Create the 'rpm' sub-parser
- parser_rpm = subparsers.add_parser('rpm', \
- help='RPM operations')
+ parser_rpm = subparsers.add_parser('rpm',
+ help='RPM operations')
subparser_rpm = parser_rpm.add_subparsers(dest='sub_command')
##################################################################
# Create the 'repo' sub-parser
- parser_repo = subparsers.add_parser('repo', \
- help='Repo operations')
+ parser_repo = subparsers.add_parser('repo',
+ help='Repo operations')
subparser_repo = parser_repo.add_subparsers(dest='sub_command')
##################################################################
# Create the 'cart create' sub-parser
- parser_cart_create = subparser_cart.add_parser('create', \
- help='Create a cart with the items specified.', \
- usage='%(prog)s CARTNAME [-f rpm-manifest] ... [-r REPONAME items ... [ -r REPONAME items ...]]')
+ parser_cart_create = subparser_cart.add_parser('create',
+ help='Create a cart with the items specified.',
+ usage='%(prog)s CARTNAME [-f rpm-manifest] ... [-r REPONAME items ... [ -r REPONAME items ...]]')
- parser_cart_create.add_argument('cartname', metavar='cart-name', \
- help='Cart name')
+ parser_cart_create.add_argument('cartname', metavar='cart-name',
+ help='Cart name')
cgroup = parser_cart_create.add_mutually_exclusive_group(required=True)
- cgroup.add_argument('-r', metavar=('reponame', 'item'), \
- action='append', \
- nargs='+', \
- help='Destination repo name')
+ cgroup.add_argument('-r', metavar=('reponame', 'item'),
+ action='append',
+ nargs='+',
+ help='Destination repo name')
- cgroup.add_argument('-f', metavar='rpm-manifest', \
- action='append', \
- help='RPM manifest for cart')
+ cgroup.add_argument('-f', metavar='rpm-manifest',
+ action='append',
+ help='RPM manifest for cart')
parser_cart_create.set_defaults(j=juicer.juicer.create)
##################################################################
# Create the 'edit' sub-parser
- # parser_edit = subparsers.add_parser('edit', \
+ # parser_edit = subparsers.add_parser('edit',
# help='Interactively edit a release cart.')
- # parser_edit.add_argument('cart-name', metavar='cartname', \
+ # parser_edit.add_argument('cart-name', metavar='cartname',
# help='The name of your release cart')
# parser_edit.set_defaults(j=juicer.juicer.edit)
##################################################################
# Create the 'cart show' sub-parser
- parser_cart_show = subparser_cart.add_parser('show', \
- usage='%(prog)s CARTNAME [--in [environment [environment ...]]] [-h]', \
- help='Print the contents of a cart.')
+ parser_cart_show = subparser_cart.add_parser('show',
+ usage='%(prog)s CARTNAME [--in [environment [environment ...]]] [-h]',
+ help='Print the contents of a cart.')
- parser_cart_show.add_argument('cartname', metavar='name', \
- help='The name of your cart')
+ parser_cart_show.add_argument('cartname', metavar='name',
+ help='The name of your cart')
- parser_cart_show.add_argument('--in', nargs='*', \
- metavar='environment', \
- default=juicer.utils.get_environments(), \
- help='Only show carts pushed to the given environment.', \
- dest='environment')
+ parser_cart_show.add_argument('--in', nargs='*',
+ metavar='environment',
+ default=juicer.utils.get_environments(),
+ help='Only show carts pushed to the given environment.',
+ dest='environment')
parser_cart_show.set_defaults(j=juicer.juicer.show)
##################################################################
# Create the 'cart list' sub-parser
- parser_cart_list = subparser_cart.add_parser('list', \
- help='List all of your carts.')
+ parser_cart_list = subparser_cart.add_parser('list',
+ help='List all of your carts.')
- parser_cart_list.add_argument('cart_glob', metavar='cart_glob', \
- nargs='*', default=['*'], \
- help='A pattern to match cart names against (default: *)')
+ parser_cart_list.add_argument('cart_glob', metavar='cart_glob',
+ nargs='*', default=['*'],
+ help='A pattern to match cart names against (default: *)')
parser_cart_list.set_defaults(j=juicer.juicer.list)
##################################################################
# Create the 'cart update' sub-parser
- parser_cart_update = subparser_cart.add_parser('update', \
- help='Update a release cart with items.', \
- usage='%(prog)s CARTNAME [-f rpm-manifest] ... [-r REPONAME items ... [-r REPONAME items...]]')
+ parser_cart_update = subparser_cart.add_parser('update',
+ help='Update a release cart with items.',
+ usage='%(prog)s CARTNAME [-f rpm-manifest] ... [-r REPONAME items ... [-r REPONAME items...]]')
- parser_cart_update.add_argument('cartname', metavar='cartname', \
- help='The name of your release cart')
+ parser_cart_update.add_argument('cartname', metavar='cartname',
+ help='The name of your release cart')
- parser_cart_update.add_argument('-r', metavar=('reponame', 'item'), \
- action='append', \
- nargs='+', \
- help='Destination repo name')
+ parser_cart_update.add_argument('-r', metavar=('reponame', 'item'),
+ action='append',
+ nargs='+',
+ help='Destination repo name')
- parser_cart_update.add_argument('-f', metavar='rpm-manifest', \
- action='append', \
- help='RPM manifest for cart')
+ parser_cart_update.add_argument('-f', metavar='rpm-manifest',
+ action='append',
+ help='RPM manifest for cart')
parser_cart_update.set_defaults(j=juicer.juicer.update)
##################################################################
# Create the 'cart pull' sub-parser
- parser_cart_pull = subparser_cart.add_parser('pull', \
- help='Pull a release cart from remote.')
+ parser_cart_pull = subparser_cart.add_parser('pull',
+ help='Pull a release cart from remote.')
- parser_cart_pull.add_argument('cartname', metavar='cartname', \
- help='The name of your release cart')
+ parser_cart_pull.add_argument('cartname', metavar='cartname',
+ help='The name of your release cart')
parser_cart_pull.set_defaults(j=juicer.juicer.pull)
##################################################################
# Create the 'create-like' sub-parser
- # parser_createlike = subparsers.add_parser('create-like', \
+ # parser_createlike = subparsers.add_parser('create-like',
# help='Create a new cart based off an existing one.')
- # parser_createlike.add_argument('cart-name', metavar='cartname', \
+ # parser_createlike.add_argument('cart-name', metavar='cartname',
# help='The name of your new release cart')
- # parser_createlike.add_argument('old-cart-name', \
+ # parser_createlike.add_argument('old-cart-name',
# metavar='oldcartname', help='Cart to copy')
- # parser_createlike.add_argument('items', metavar='items', \
- # nargs="+", \
+ # parser_createlike.add_argument('items', metavar='items',
+ # nargs="+",
# help='Cart name')
# parser_createlike.set_defaults(j=juicer.juicer.createlike)
##################################################################
# Create the 'cart push' sub-parser
- parser_cart_push = subparser_cart.add_parser('push', \
- help='Pushes/Updates a cart on the pulp server.',
- usage='%(prog)s CARTNAME [--in [environment [environment ...]]] [-h]')
+ parser_cart_push = subparser_cart.add_parser('push',
+ help='Pushes/Updates a cart on the pulp server.',
+ usage='%(prog)s CARTNAME [--in [environment [environment ...]]] [-h]')
- parser_cart_push.add_argument('cartname', metavar='cartname', \
- help='The name of your new release cart')
+ parser_cart_push.add_argument('cartname', metavar='cartname',
+ help='The name of your new release cart')
- parser_cart_push.add_argument('--in', nargs='*', \
- metavar='environment', \
- default=[self._default_start_in], \
- help='The environments to push into.', \
- dest='environment')
+ parser_cart_push.add_argument('--in', nargs='*',
+ metavar='environment',
+ default=[self._default_start_in],
+ help='The environments to push into.',
+ dest='environment')
parser_cart_push.set_defaults(j=juicer.juicer.push)
##################################################################
# Create the 'rpm search' sub-parser
- parser_rpm_search = subparser_rpm.add_parser('search', \
- help='Search for an RPM in pulp.', \
- usage='%(prog)s rpmname [-r repo [repo]] [-c] [--in environment [environment]] [-h]')
+ parser_rpm_search = subparser_rpm.add_parser('search',
+ help='Search for an RPM in pulp.',
+ usage='%(prog)s rpmname [-r repo [repo]] [-c] [--in environment [environment]] [-h]')
- parser_rpm_search.add_argument('rpmname', metavar='rpmname', \
- help='The name of the rpm(s) to search for.')
+ parser_rpm_search.add_argument('rpmname', metavar='rpmname',
+ help='The name of the rpm(s) to search for.')
- parser_rpm_search.add_argument('-r', nargs='*', metavar='repos', \
- default=[], help='The repo(s) to limit search scope to.')
+ parser_rpm_search.add_argument('-r', nargs='*', metavar='repos',
+ default=[], help='The repo(s) to limit search scope to.')
- parser_rpm_search.add_argument('-c', '--carts', dest='carts', \
- action='store_true', \
- help="Search for the package in carts as well")
+ parser_rpm_search.add_argument('-c', '--carts', dest='carts',
+ action='store_true',
+ help="Search for the package in carts as well")
- parser_rpm_search.add_argument('--in', nargs='*', \
- metavar='environment', \
- default=[self._default_start_in], \
- help='The environments to limit search scope to.', \
- dest='environment')
+ parser_rpm_search.add_argument('--in', nargs='*',
+ metavar='environment',
+ default=[self._default_start_in],
+ help='The environments to limit search scope to.',
+ dest='environment')
parser_rpm_search.set_defaults(j=juicer.juicer.search)
##################################################################
# create the 'rpm upload' sub-parser
- parser_rpm_upload = subparser_rpm.add_parser('upload', \
- help='Upload the items specified into repos.', \
- usage='%(prog)s -r REPONAME items ... [ -r REPONAME items ...] [--in ENV ...]')
-
- parser_rpm_upload.add_argument('-r', metavar=('reponame', 'item'), \
- action='append', \
- nargs='+', \
- required=True, \
+ parser_rpm_upload = subparser_rpm.add_parser('upload',
+ help='Upload the items specified into repos.',
+ usage='%(prog)s -r REPONAME items ... [ -r REPONAME items ...] [--in ENV ...]')
+
+ parser_rpm_upload.add_argument('-r', metavar=('reponame', 'item'),
+ action='append',
+ nargs='+',
+ required=True,
help='Destination repo name, items...')
- parser_rpm_upload.add_argument('--in', nargs='*', \
- metavar='environment', \
- default=[self._default_start_in], \
- help='The environments to upload into.', \
- dest='environment')
+ parser_rpm_upload.add_argument('--in', nargs='*',
+ metavar='environment',
+ default=[self._default_start_in],
+ help='The environments to upload into.',
+ dest='environment')
parser_rpm_upload.set_defaults(j=juicer.juicer.upload)
##################################################################
# create the 'hello' sub-parser
- parser_hello = subparsers.add_parser('hello', \
- help='Test your connection to the pulp server', \
- usage='%(prog)s hello [--in env ...]')
+ parser_hello = subparsers.add_parser('hello',
+ help='Test your connection to the pulp server',
+ usage='%(prog)s hello [--in env ...]')
- parser_hello.add_argument('--in', nargs='*', \
- metavar='environment', \
- help='The environments to test the connection to.', \
- default=self._default_envs, \
- dest='environment')
+ parser_hello.add_argument('--in', nargs='*',
+ metavar='environment',
+ help='The environments to test the connection to.',
+ default=self._default_envs,
+ dest='environment')
parser_hello.set_defaults(j=juicer.juicer.hello)
##################################################################
# create the 'cart promote' sub-parser
- parser_cart_promote = subparser_cart.add_parser('promote', \
- help='Promote a cart to the next environment')
+ parser_cart_promote = subparser_cart.add_parser('promote',
+ help='Promote a cart to the next environment')
- parser_cart_promote.add_argument('cartname', metavar='cart', \
- help='The name of the cart to promote')
+ parser_cart_promote.add_argument('cartname', metavar='cart',
+ help='The name of the cart to promote')
parser_cart_promote.set_defaults(j=juicer.juicer.promote)
##################################################################
# create the 'cart merge' sub-parser
- parser_cart_merge = subparser_cart.add_parser('merge', \
- help='Merge the contents of two carts', \
- usage='%(prog)s merge CART1 CART2 [CARTN ...]]] --into NEWCART')
+ parser_cart_merge = subparser_cart.add_parser('merge',
+ help='Merge the contents of two carts',
+ usage='%(prog)s merge CART1 CART2 [CARTN ...]]] --into NEWCART')
parser_cart_merge.add_argument('carts', nargs="+",
- metavar='carts', \
- help='Two or more carts to merge')
+ metavar='carts',
+ help='Two or more carts to merge')
parser_cart_merge.add_argument('--into', '-i',
- metavar='new_cart_name', \
- help='Name of resultant cart, defaults to updating CART1')
+ metavar='new_cart_name',
+ help='Name of resultant cart, defaults to updating CART1')
parser_cart_merge.set_defaults(j=juicer.juicer.merge)
##################################################################
# create the 'rpm delete' sub-parser
- parser_rpm_delete = subparser_rpm.add_parser('delete', \
- help='Remove rpm(s) from repositories', \
- usage='%(prog)s -r REPO-NAME ITEM ITEM ... --in [ENV ...]')
-
- parser_rpm_delete.add_argument('-r', metavar=('reponame', 'item'), \
- required=True, \
- action='append', \
- nargs='+', \
+ parser_rpm_delete = subparser_rpm.add_parser('delete',
+ help='Remove rpm(s) from repositories',
+ usage='%(prog)s -r REPO-NAME ITEM ITEM ... --in [ENV ...]')
+
+ parser_rpm_delete.add_argument('-r', metavar=('reponame', 'item'),
+ required=True,
+ action='append',
+ nargs='+',
help='Target repo filename, filename...')
- parser_rpm_delete.add_argument('--in', nargs='*', \
- metavar='environment', \
- help='The environments to test the connection to.', \
- default=self._default_envs, \
- dest='environment')
+ parser_rpm_delete.add_argument('--in', nargs='*',
+ metavar='environment',
+ help='The environments to test the connection to.',
+ default=self._default_envs,
+ dest='environment')
parser_rpm_delete.set_defaults(j=juicer.juicer.delete_rpm)
##################################################################
# create the 'publish' sub-parser
- parser_repo_publish = subparser_repo.add_parser('publish', \
- help='Publish a repository, this will regenerate metadata.', \
- usage='%(prog)s publish REPO --in [ENV ...]')
-
- parser_repo_publish.add_argument('repo', metavar='reponame', \
- help='Target repo to publish.')
-
- parser_repo_publish.add_argument('--in', nargs='*', \
- metavar='environment', \
- help='The environments to publish repository in.', \
- default=self._default_envs, \
- dest='environment')
+ parser_repo_publish = subparser_repo.add_parser('publish',
+ help='Publish a repository, this will regenerate metadata.',
+ usage='%(prog)s publish REPO --in [ENV ...]')
+
+ parser_repo_publish.add_argument('repo', metavar='reponame',
+ help='Target repo to publish.')
+
+ parser_repo_publish.add_argument('--in', nargs='*',
+ metavar='environment',
+ help='The environments to publish repository in.',
+ default=self._default_envs,
+ dest='environment')
parser_repo_publish.set_defaults(j=juicer.juicer.publish)
diff --git a/juicer/utils/ValidateRepoDef.py b/juicer/utils/ValidateRepoDef.py
new file mode 100644
index 0000000..d301e06
--- /dev/null
+++ b/juicer/utils/ValidateRepoDef.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+# Juicer - Administer Pulp and Release Carts
+# Copyright © 2014, 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 3 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/>.
+
+
+"""
+Repository Definition file validation functions.
+
+- Start by passing a document path to `validate_document`.
+
+- Wrap in a try/except
+
+- except for `JuicerRepoDefError`
+"""
+
+
+from juicer.common.Errors import *
+import json
+import juicer.utils
+import juicer.common.Constants
+import re
+import sys
+
+valid_repo_name = re.compile('^([a-zA-Z0-9-_.]+)$')
+
+def validate_document(document_path):
+ try:
+ defs = juicer.utils.read_json_document(document_path)
+ except ValueError, e:
+ raise JuicerRepoDefError("Could not validate: %s" % str(e))
+
+ if is_list(defs):
+ for repo_def in defs:
+ validate_definitions(defs)
+ else:
+ raise JuicerRepoDefError("Document is not a list")
+
+ return defs
+
+def is_list(ds):
+ return type(ds) == list
+
+def is_dict(ds):
+ return type(ds) == dict
+
+def is_valid_checksum(cs):
+ return cs in juicer.common.Constants.REPO_DEF_CHECKSUM_TYPES
+
+def has_valid_keys(ds):
+ keys = ds.keys()
+ required_keys = juicer.common.Constants.REPO_DEF_REQ_KEYS
+ optional_keys = juicer.common.Constants.REPO_DEF_OPT_KEYS
+ if set(required_keys) & set(keys):
+ remaining_keys = set(keys) - set(required_keys)
+ if len(set(remaining_keys) - set(optional_keys)) > 0:
+ raise JuicerRepoDefError("Extra keys found: %s" % (', '.join(set(remaining_keys) - set(optional_keys))))
+
+ if 'env' in keys:
+ if not is_list(ds['env']):
+ raise JuicerRepoDefError("%s is not a list" % type(ds['env']))
+ else:
+ pass
+
+ if 'checksum_type' in keys:
+ if not is_valid_checksum(ds['checksum_type']):
+ raise JuicerRepoDefError("Invalid checksum_type in repo def: %s" % ds['checksum_type'])
+ else:
+ pass
+
+ if 'feed' in keys:
+ if not is_string(ds['feed']):
+ raise JuicerRepoDefError("%s is not a string" % type(ds['feed']))
+ else:
+ pass
+
+ return True
+ else:
+ raise JuicerRepoDefError("Missing required keys: %s" % ', '.join(required_keys))
+
+def is_valid_repo_name(repo_name):
+ match = valid_repo_name.match(repo_name)
+ if match:
+ return True
+ else:
+ return False
+
+def is_string(str):
+ return (type(str) == str) or (type(str) == unicode)
+
+def validate_definitions(defs):
+ for definition in defs:
+ if is_dict(definition):
+ validate_def_keys(definition)
+ else:
+ raise JuicerRepoDefError("Repo definition is not a dictionary!")
+
+def validate_def_keys(definition):
+ if has_valid_keys(definition):
+ if not is_valid_repo_name(definition['name']):
+ raise JuicerRepoDefError("Repo name is invalid: %s" % definition['name'])
+ else:
+ raise JuicerRepoDefError("Repo definition has invalid keys")
+
+if __name__ == '__main__':
+ passes = 0
+ failures = 0
+
+ for f in sys.argv[1:]:
+ try:
+ validate_document(f)
+ except JuicerRepoDefError, e:
+ failures += 1
+ print "\033[00;31mFAIL:\033[0m %s: %s" % (f, str(e))
+ else:
+ passes += 1
+ print "\033[00;32mPASS:\033[0m %s" % f
+
+ sys.exit(failures)
diff --git a/juicer/utils/__init__.py b/juicer/utils/__init__.py
index e9e4119..5dd377a 100644
--- a/juicer/utils/__init__.py
+++ b/juicer/utils/__init__.py
@@ -26,6 +26,7 @@ import cStringIO
import fnmatch
import juicer.utils.Log
import juicer.utils.Remotes
+import juicer.common.Repo
import os
import os.path
import rpm
@@ -34,11 +35,11 @@ import sys
import requests
import shutil
import re
+import texttable
import urllib2
import yaml
try:
import json
- json
except ImportError:
import simplejson as json
from pymongo import Connection as MongoClient
@@ -193,8 +194,7 @@ def get_environments():
juicer.utils.Log.log_debug("Reading environment sections:")
environments = config.sections()
-
- juicer.utils.Log.log_notice("Read environment sections: %s", environments)
+ juicer.utils.Log.log_debug("Read environment sections: %s", ', '.join(environments))
return environments
@@ -764,9 +764,59 @@ def find_latest(pkg_name, url='/content/units/rpm/search/'):
return pkg_info['version'], pkg_info['release']
-
def juicer_version():
"""
Duh, just print out what version of juicer you're running.
"""
return juicer.__version__
+
+def header(msg):
+ """
+ Wrap `msg` in bars to create a header effect
+ """
+ # Accounting for '| ' and ' |'
+ width = len(msg) + 4
+ s = []
+ s.append('-' * width)
+ s.append("| %s |" % msg)
+ s.append('-' * width)
+ return '\n'.join(s)
+
+def table(rows):
+ t = texttable.Texttable()
+ t.add_rows(rows)
+ return t.draw()
+
+
+def unique_repo_def_envs(repo_def):
+ defined_envs = set()
+ for repo in repo_def:
+ defined_envs = defined_envs.union(repo['env'])
+ juicer.utils.Log.log_debug("envs for repo %s: %s", repo['name'], ", ".join(repo['env']))
+ return defined_envs
+
+def repo_exists_in_repo_list(repo, repo_list):
+ """`repo_def` - a Repo object representing a juicer repo def
+
+ `repo_list` a list of repo names (sans environment-id), per the
+ data from juicer.admin.JuicerAdmin.list_repos
+ """
+ return repo['name'] in repo_list
+
+def repo_in_defined_envs(repo, all_envs):
+ """Raises exception if the repo references undefined environments"""
+ remaining_envs = set(repo['env']) - set(all_envs)
+ if set(repo['env']) - set(all_envs):
+ raise JuicerRepoInUndefinedEnvs("Repo def %s references undefined environments: %s" %
+ (repo['name'], ", ".join(list(remaining_envs))))
+ else:
+ return True
+
+def repo_def_matches_reality(juicer_def, pulp_def):
+ """Compare a juicer repo def with a given pulp definition. Compute and
+ return the update necessary to make `pulp_def` match `juicer_def`.
+
+ `juicer_def` - A JuicerRepo() object representing a juicer repository
+ `pulp_def` - A PulpRepo() object representing a pulp repository
+ """
+ return juicer.common.Repo.RepoDiff(juicer_repo=juicer_def, pulp_repo=pulp_def)
diff --git a/juicer/utils/texttable.py b/juicer/utils/texttable.py
new file mode 100644
index 0000000..c817910
--- /dev/null
+++ b/juicer/utils/texttable.py
@@ -0,0 +1,473 @@
+#!/usr/bin/env python
+#
+# texttable - module for creating simple ASCII tables
+# Copyright (C) 2003-2009 Gerome Fournier <jefke(at)free.fr>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""module for creating simple ASCII tables
+
+
+Example:
+
+ table = Texttable()
+ table.set_cols_align(["l", "r", "c"])
+ table.set_cols_valign(["t", "m", "b"])
+ table.add_rows([ ["Name", "Age", "Nickname"],
+ ["Mr\\nXavier\\nHuon", 32, "Xav'"],
+ ["Mr\\nBaptiste\\nClement", 1, "Baby"] ])
+ print table.draw()
+
+Result:
+
+ +----------+-----+----------+
+ | Name | Age | Nickname |
+ +==========+=====+==========+
+ | Mr | | |
+ | Xavier | 32 | |
+ | Huon | | Xav' |
+ +----------+-----+----------+
+ | Mr | | |
+ | Baptiste | 1 | |
+ | Clement | | Baby |
+ +----------+-----+----------+
+"""
+
+__all__ = ["Texttable", "ArraySizeError"]
+
+__author__ = 'Gerome Fournier <jefke(at)free.fr>'
+__license__ = 'GPL'
+__version__ = '0.7'
+__revision__ = '$Id: texttable.py 128 2009-10-04 15:16:22Z jef $'
+__credits__ = """\
+Jeff Kowalczyk:
+ - textwrap improved import
+ - comment concerning header output
+
+Anonymous:
+ - add_rows method, for adding rows in one go
+
+Sergey Simonenko:
+ - redefined len() function to deal with non-ASCII characters
+
+"""
+
+import sys
+import string
+
+try:
+ if sys.version >= '2.3':
+ import textwrap
+ elif sys.version >= '2.2':
+ from optparse import textwrap
+ else:
+ from optik import textwrap
+except ImportError:
+ sys.stderr.write("Can't import textwrap module!\n")
+ raise
+
+try:
+ True, False
+except NameError:
+ (True, False) = (1, 0)
+
+def len(iterable):
+ """Redefining len here so it will be able to work with non-ASCII characters
+ """
+ if not isinstance(iterable, str):
+ return iterable.__len__()
+
+ try:
+ return len(unicode(iterable, 'utf'))
+ except:
+ return iterable.__len__()
+
+class ArraySizeError(Exception):
+ """Exception raised when specified rows don't fit the required size
+ """
+
+ def __init__(self, msg):
+ self.msg = msg
+ Exception.__init__(self, msg, '')
+
+ def __str__(self):
+ return self.msg
+
+class Texttable:
+
+ BORDER = 1
+ HEADER = 1 << 1
+ HLINES = 1 << 2
+ VLINES = 1 << 3
+
+ def __init__(self, max_width=80):
+ """Constructor
+
+ - max_width is an integer, specifying the maximum width of the table
+ - if set to 0, size is unlimited, therefore cells won't be wrapped
+ """
+
+ if max_width <= 0:
+ max_width = False
+ self._max_width = max_width
+ self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \
+ Texttable.HEADER
+ self.set_chars(['-', '|', '+', '='])
+ self.reset()
+
+ def reset(self):
+ """Reset the instance
+
+ - reset rows and header
+ """
+
+ self._hline_string = None
+ self._row_size = None
+ self._header = []
+ self._rows = []
+
+ def header(self, array):
+ """Specify the header of the table
+ """
+
+ self._check_row_size(array)
+ self._header = map(str, array)
+
+ def add_row(self, array):
+ """Add a row in the rows stack
+
+ - cells can contain newlines and tabs
+ """
+
+ self._check_row_size(array)
+ self._rows.append(map(str, array))
+
+ def add_rows(self, rows, header=True):
+ """Add several rows in the rows stack
+
+ - The 'rows' argument can be either an iterator returning arrays,
+ or a by-dimensional array
+ - 'header' specifies if the first row should be used as the header
+ of the table
+ """
+
+ # nb: don't use 'iter' on by-dimensional arrays, to get a
+ # usable code for python 2.1
+ if header:
+ if hasattr(rows, '__iter__') and hasattr(rows, 'next'):
+ self.header(rows.next())
+ else:
+ self.header(rows[0])
+ rows = rows[1:]
+ for row in rows:
+ self.add_row(row)
+
+ def set_chars(self, array):
+ """Set the characters used to draw lines between rows and columns
+
+ - the array should contain 4 fields:
+
+ [horizontal, vertical, corner, header]
+
+ - default is set to:
+
+ ['-', '|', '+', '=']
+ """
+
+ if len(array) != 4:
+ raise ArraySizeError, "array should contain 4 characters"
+ array = [ x[:1] for x in [ str(s) for s in array ] ]
+ (self._char_horiz, self._char_vert,
+ self._char_corner, self._char_header) = array
+
+ def set_deco(self, deco):
+ """Set the table decoration
+
+ - 'deco' can be a combinaison of:
+
+ Texttable.BORDER: Border around the table
+ Texttable.HEADER: Horizontal line below the header
+ Texttable.HLINES: Horizontal lines between rows
+ Texttable.VLINES: Vertical lines between columns
+
+ All of them are enabled by default
+
+ - example:
+
+ Texttable.BORDER | Texttable.HEADER
+ """
+
+ self._deco = deco
+
+ def set_cols_align(self, array):
+ """Set the desired columns alignment
+
+ - the elements of the array should be either "l", "c" or "r":
+
+ * "l": column flushed left
+ * "c": column centered
+ * "r": column flushed right
+ """
+
+ self._check_row_size(array)
+ self._align = array
+
+ def set_cols_valign(self, array):
+ """Set the desired columns vertical alignment
+
+ - the elements of the array should be either "t", "m" or "b":
+
+ * "t": column aligned on the top of the cell
+ * "m": column aligned on the middle of the cell
+ * "b": column aligned on the bottom of the cell
+ """
+
+ self._check_row_size(array)
+ self._valign = array
+
+ def set_cols_width(self, array):
+ """Set the desired columns width
+
+ - the elements of the array should be integers, specifying the
+ width of each column. For example:
+
+ [10, 20, 5]
+ """
+
+ self._check_row_size(array)
+ try:
+ array = map(int, array)
+ if reduce(min, array) <= 0:
+ raise ValueError
+ except ValueError:
+ sys.stderr.write("Wrong argument in column width specification\n")
+ raise
+ self._width = array
+
+ def draw(self):
+ """Draw the table
+
+ - the table is returned as a whole string
+ """
+
+ if not self._header and not self._rows:
+ return
+ self._compute_cols_width()
+ self._check_align()
+ out = ""
+ if self._has_border():
+ out += self._hline()
+ if self._header:
+ out += self._draw_line(self._header, isheader=True)
+ if self._has_header():
+ out += self._hline_header()
+ length = 0
+ for row in self._rows:
+ length += 1
+ out += self._draw_line(row)
+ if self._has_hlines() and length < len(self._rows):
+ out += self._hline()
+ if self._has_border():
+ out += self._hline()
+ return out[:-1]
+
+ def _check_row_size(self, array):
+ """Check that the specified array fits the previous rows size
+ """
+
+ if not self._row_size:
+ self._row_size = len(array)
+ elif self._row_size != len(array):
+ raise ArraySizeError, "array should contain %d elements" \
+ % self._row_size
+
+ def _has_vlines(self):
+ """Return a boolean, if vlines are required or not
+ """
+
+ return self._deco & Texttable.VLINES > 0
+
+ def _has_hlines(self):
+ """Return a boolean, if hlines are required or not
+ """
+
+ return self._deco & Texttable.HLINES > 0
+
+ def _has_border(self):
+ """Return a boolean, if border is required or not
+ """
+
+ return self._deco & Texttable.BORDER > 0
+
+ def _has_header(self):
+ """Return a boolean, if header line is required or not
+ """
+
+ return self._deco & Texttable.HEADER > 0
+
+ def _hline_header(self):
+ """Print header's horizontal line
+ """
+
+ return self._build_hline(True)
+
+ def _hline(self):
+ """Print an horizontal line
+ """
+
+ if not self._hline_string:
+ self._hline_string = self._build_hline()
+ return self._hline_string
+
+ def _build_hline(self, is_header=False):
+ """Return a string used to separated rows or separate header from
+ rows
+ """
+ horiz = self._char_horiz
+ if (is_header):
+ horiz = self._char_header
+ # compute cell separator
+ s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()],
+ horiz)
+ # build the line
+ l = string.join([horiz*n for n in self._width], s)
+ # add border if needed
+ if self._has_border():
+ l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz,
+ self._char_corner)
+ else:
+ l += "\n"
+ return l
+
+ def _len_cell(self, cell):
+ """Return the width of the cell
+
+ Special characters are taken into account to return the width of the
+ cell, such like newlines and tabs
+ """
+
+ cell_lines = cell.split('\n')
+ maxi = 0
+ for line in cell_lines:
+ length = 0
+ parts = line.split('\t')
+ for part, i in zip(parts, range(1, len(parts) + 1)):
+ length = length + len(part)
+ if i < len(parts):
+ length = (length/8 + 1)*8
+ maxi = max(maxi, length)
+ return maxi
+
+ def _compute_cols_width(self):
+ """Return an array with the width of each column
+
+ If a specific width has been specified, exit. If the total of the
+ columns width exceed the table desired width, another width will be
+ computed to fit, and cells will be wrapped.
+ """
+
+ if hasattr(self, "_width"):
+ return
+ maxi = []
+ if self._header:
+ maxi = [ self._len_cell(x) for x in self._header ]
+ for row in self._rows:
+ for cell,i in zip(row, range(len(row))):
+ try:
+ maxi[i] = max(maxi[i], self._len_cell(cell))
+ except (TypeError, IndexError):
+ maxi.append(self._len_cell(cell))
+ items = len(maxi)
+ length = reduce(lambda x,y: x+y, maxi)
+ if self._max_width and length + items*3 + 1 > self._max_width:
+ maxi = [(self._max_width - items*3 -1) / items \
+ for n in range(items)]
+ self._width = maxi
+
+ def _check_align(self):
+ """Check if alignment has been specified, set default one if not
+ """
+
+ if not hasattr(self, "_align"):
+ self._align = ["l"]*self._row_size
+ if not hasattr(self, "_valign"):
+ self._valign = ["t"]*self._row_size
+
+ def _draw_line(self, line, isheader=False):
+ """Draw a line
+
+ Loop over a single cell length, over all the cells
+ """
+
+ line = self._splitit(line, isheader)
+ space = " "
+ out = ""
+ for i in range(len(line[0])):
+ if self._has_border():
+ out += "%s " % self._char_vert
+ length = 0
+ for cell, width, align in zip(line, self._width, self._align):
+ length += 1
+ cell_line = cell[i]
+ fill = width - len(cell_line)
+ if isheader:
+ align = "c"
+ if align == "r":
+ out += "%s " % (fill * space + cell_line)
+ elif align == "c":
+ out += "%s " % (fill/2 * space + cell_line \
+ + (fill/2 + fill%2) * space)
+ else:
+ out += "%s " % (cell_line + fill * space)
+ if length < len(line):
+ out += "%s " % [space, self._char_vert][self._has_vlines()]
+ out += "%s\n" % ['', self._char_vert][self._has_border()]
+ return out
+
+ def _splitit(self, line, isheader):
+ """Split each element of line to fit the column width
+
+ Each element is turned into a list, result of the wrapping of the
+ string to the desired width
+ """
+
+ line_wrapped = []
+ for cell, width in zip(line, self._width):
+ array = []
+ for c in cell.split('\n'):
+ array.extend(textwrap.wrap(unicode(c, 'utf'), width))
+ line_wrapped.append(array)
+ max_cell_lines = reduce(max, map(len, line_wrapped))
+ for cell, valign in zip(line_wrapped, self._valign):
+ if isheader:
+ valign = "t"
+ if valign == "m":
+ missing = max_cell_lines - len(cell)
+ cell[:0] = [""] * (missing / 2)
+ cell.extend([""] * (missing / 2 + missing % 2))
+ elif valign == "b":
+ cell[:0] = [""] * (max_cell_lines - len(cell))
+ else:
+ cell.extend([""] * (max_cell_lines - len(cell)))
+ return line_wrapped
+
+if __name__ == '__main__':
+ table = Texttable()
+ table.set_cols_align(["l", "r", "c"])
+ table.set_cols_valign(["t", "m", "b"])
+ table.add_rows([ ["Name", "Age", "Nickname"],
+ ["Mr\nXavier\nHuon", 32, "Xav'"],
+ ["Mr\nBaptiste\nClement", 1, "Baby"] ])
+ print table.draw()
diff --git a/setup.py.in b/setup.py.in
index 55ed36c..e8fa96b 100644
--- a/setup.py.in
+++ b/setup.py.in
@@ -7,7 +7,7 @@ setup(name='juicer',
version='%VERSION%',
description='Administer Pulp and Release Carts',
maintainer='Tim Bielawa',
- maintainer_email='tim@redhat.com',
+ maintainer_email='tbielawa@redhat.com',
url='https://github.com/juicer/juicer',
license='GPLv3+',
package_dir={ 'juicer': 'juicer' },
diff --git a/share/juicer/repo_def_example.json b/share/juicer/repo_def_example.json
new file mode 100644
index 0000000..d417af4
--- /dev/null
+++ b/share/juicer/repo_def_example.json
@@ -0,0 +1,14 @@
+[
+ {
+ "name": "repodef01",
+ "checksum_type": "sha",
+ "env": ["re", "qa", "stage", "prod"]
+ },
+ {
+ "name": "repodef02"
+ },
+ {
+ "name": "repodef03",
+ "env": ["re"]
+ }
+]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment