File spacecmd-git-0.6553218.obscpio of Package spacecmd.12967

07070100000000000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000000900000000spacecmd07070100000001000081B40000000000000000000000015DA8415F00000010000000000000000000000000000000000000001400000000spacecmd/.gitignore*~
*.pyc
*.swp

07070100000002000081B40000000000000000000000015DA8415F0000048A000000000000000000000000000000000000001900000000spacecmd/Makefile.pythonTHIS_MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
CURRENT_DIR := $(dir $(THIS_MAKEFILE))
include $(CURRENT_DIR)../rel-eng/Makefile.python

# Docker tests variables
DOCKER_CONTAINER_BASE = uyuni-master
DOCKER_REGISTRY       = registry.mgr.suse.de
DOCKER_RUN_EXPORT     = "PYTHONPATH=$PYTHONPATH"
DOCKER_VOLUMES        = -v "$(CURDIR)/../:/manager"

__pylint ::
	$(call update_pip_env)
	pylint --rcfile=pylintrc $(shell find -name '*.py') > reports/pylint.log || true

__pytest ::
	$(call update_pip_env)
	$(call install_pytest)
	$(call install_by_setup, '.')
	cd tests
	pytest --disable-warnings --tb=native --color=yes -v

docker_pylint ::
	docker run --rm -e $(DOCKER_RUN_EXPORT) $(DOCKER_VOLUMES) $(DOCKER_REGISTRY)/$(DOCKER_CONTAINER_BASE)-pgsql /bin/sh -c "cd /manager/spacecmd; make -f Makefile.python __pylint"

docker_pytest ::
	docker run --rm -e $(DOCKER_RUN_EXPORT) $(DOCKER_VOLUMES) $(DOCKER_REGISTRY)/$(DOCKER_CONTAINER_BASE)-pgsql /bin/sh -c "cd /manager/spacecmd; make -f Makefile.python __pytest"

docker_shell ::
	docker run -t -i --rm -e $(DOCKER_RUN_EXPORT) $(DOCKER_VOLUMES) $(DOCKER_REGISTRY)/$(DOCKER_CONTAINER_BASE)-pgsql /bin/bash
07070100000003000081B40000000000000000000000015DA8415F00000359000000000000000000000000000000000000001B00000000spacecmd/Makefile.spacecmdTHIS_MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
CURRENT_DIR := $(dir $(THIS_MAKEFILE))
include $(CURRENT_DIR)../rel-eng/Makefile.python

# Docker tests variables
DOCKER_CONTAINER_BASE = uyuni-master
DOCKER_REGISTRY       = registry.mgr.suse.de
DOCKER_RUN_EXPORT     = "PYTHONPATH=/manager/client/rhel/rhnlib/:/manager/client/rhel/rhn-client-tools/src"
DOCKER_VOLUMES        = -v "$(CURDIR)/../:/manager"

__pylint ::
	$(call update_pip_env)
	pylint --rcfile=spacecmd-pylintrc src/* > reports/pylint.log || :

docker_pylint ::
	docker run --rm -e $(DOCKER_RUN_EXPORT) $(DOCKER_VOLUMES) $(DOCKER_REGISTRY)/$(DOCKER_CONTAINER_BASE)-pgsql /bin/sh -c "cd /manager/spacecmd; make -f Makefile.spacecmd __pylint"

docker_shell ::
	docker run -t -i --rm -e $(DOCKER_RUN_EXPORT) $(DOCKER_VOLUMES) $(DOCKER_REGISTRY)/$(DOCKER_CONTAINER_BASE)-pgsql /bin/bash
07070100000004000081B40000000000000000000000015DA8415F00001396000000000000000000000000000000000000001200000000spacecmd/pylintrc# spacecmd package pylint configuration

[MASTER]

# Profiled execution.
profile=no

# Pickle collected data for later comparisons.
persistent=no


[MESSAGES CONTROL]

# Disable the message(s) with the given id(s).


disable=I0011,
	C0302,
	C0111,
	R0801,
	R0902,
	R0903,
	R0904,
	R0912,
	R0913,
	R0914,
	R0915,
	R0921,
	R0922,
	W0142,
	W0403,
	W0603,
	C1001,
	W0121,
	useless-else-on-loop,
	bad-whitespace,
	unpacking-non-sequence,
	superfluous-parens,
	cyclic-import,
	redefined-variable-type,
	no-else-return,

        # Uyuni disabled
	E0203,
	E0611,
	E1101,
	E1102

# list of disabled messages:
#I0011: 62: Locally disabling R0201
#C0302:  1: Too many lines in module (2425)
#C0111:  1: Missing docstring
#R0902: 19:RequestedChannels: Too many instance attributes (9/7)
#R0903:  Too few public methods
#R0904: 26:Transport: Too many public methods (22/20)
#R0912:171:set_slots_from_cert: Too many branches (59/20)
#R0913:101:GETServer.__init__: Too many arguments (11/10)
#R0914:171:set_slots_from_cert: Too many local variables (38/20)
#R0915:171:set_slots_from_cert: Too many statements (169/50)
#W0142:228:MPM_Package.write: Used * or ** magic
#W0403: 28: Relative import 'rhnLog', should be 'backend.common.rhnLog'
#W0603: 72:initLOG: Using the global statement
# for pylint-1.0 we also disable
#C1001: 46, 0: Old-style class defined. (old-style-class)
#W0121: 33,16: Use raise ErrorClass(args) instead of raise ErrorClass, args. (old-raise-syntax)
#W:243, 8: Else clause on loop without a break statement (useless-else-on-loop)
# pylint-1.1 checks
#C:334, 0: No space allowed after bracket (bad-whitespace)
#W:162, 8: Attempting to unpack a non-sequence defined at line 6 of (unpacking-non-sequence)
#C: 37, 0: Unnecessary parens after 'not' keyword (superfluous-parens)
#C:301, 0: Unnecessary parens after 'if' keyword (superfluous-parens)

[REPORTS]

# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=parseable

# Include message's id in output
include-ids=yes

# Tells whether to display a full report or only the messages
reports=yes

# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"

[VARIABLES]

# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy


[BASIC]

# Regular expression which should only match correct module names
#module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
module-rgx=([a-zA-Z_][a-zA-Z0-9_]+)$

# Regular expression which should only match correct module level names
const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$

# Regular expression which should only match correct class names
class-rgx=[a-zA-Z_][a-zA-Z0-9_]+$

# Regular expression which should only match correct function names
function-rgx=[a-z_][a-zA-Z0-9_]{,42}$

# Regular expression which should only match correct method names
method-rgx=[a-z_][a-zA-Z0-9_]{,42}$

# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$

# Regular expression which should only match correct class sttribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,42}|(__.*__))$

# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_

# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata

# List of builtins function names that should not be used, separated by a comma
bad-functions=apply,input


[DESIGN]

# Maximum number of arguments for function / method
max-args=10

# Maximum number of locals for function / method body
max-locals=20

# Maximum number of return / yield for function / method body
max-returns=6

# Maximum number of branch for function / method body
max-branchs=20

# Maximum number of statements in function / method body
max-statements=50

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of attributes for a class (see R0902).
max-attributes=7

# Minimum number of public methods for a class (see R0903).
min-public-methods=1

# Maximum number of public methods for a class (see R0904).
max-public-methods=20


[CLASSES]


[FORMAT]

# Maximum number of characters on a single line.
max-line-length=120

# Maximum number of lines in a module
max-module-lines=1000

# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string='    '


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=
07070100000005000081B40000000000000000000000015DA8415F0000032B000000000000000000000000000000000000001200000000spacecmd/setup.py# coding: utf-8
"""
Setup file.
"""
import os
from setuptools import setup, find_packages


def get_version_changelog():
    """
    Get a version from the current changelog.
    """
    changelog = None
    version = "4.0.16"
    for fname in os.listdir(os.path.dirname(os.path.abspath(__file__))):
        if fname.endswith(".changes"):
            changelog = fname
            break

    if changelog:
        with open(changelog, "r") as hcl:
            for line in hcl.readlines():
                if "version" in line:
                    version = line.split(" ")[-1]  # Typically version is the last one
                    break
    return version

setup(
    name='spacecmd',
    version=get_version_changelog(),
    packages=find_packages(where="src"),
    package_dir={
        "": "src",
    }
)
07070100000006000081B40000000000000000000000015DA8415F0000138F000000000000000000000000000000000000001B00000000spacecmd/spacecmd-pylintrc# spacewalk pylint configuration

[MASTER]

# Profiled execution.
profile=no

# Pickle collected data for later comparisons.
persistent=no


[MESSAGES CONTROL]

# Disable the message(s) with the given id(s).


disable=I0011,
	C0302,
	C0111,
	R0801,
	R0902,
	R0903,
	R0904,
	R0912,
	R0913,
	R0914,
	R0915,
	R0921,
	R0922,
	W0142,
	W0403,
	W0603,
	C1001,
	W0121,
	useless-else-on-loop,
	bad-whitespace,
	unpacking-non-sequence,
	superfluous-parens,
	cyclic-import,
	redefined-variable-type,
	no-else-return,

        # Uyuni disabled
	E0203,
	E0611,
	E1101,
	E1102

# list of disabled messages:
#I0011: 62: Locally disabling R0201
#C0302:  1: Too many lines in module (2425)
#C0111:  1: Missing docstring
#R0902: 19:RequestedChannels: Too many instance attributes (9/7)
#R0903:  Too few public methods
#R0904: 26:Transport: Too many public methods (22/20)
#R0912:171:set_slots_from_cert: Too many branches (59/20)
#R0913:101:GETServer.__init__: Too many arguments (11/10)
#R0914:171:set_slots_from_cert: Too many local variables (38/20)
#R0915:171:set_slots_from_cert: Too many statements (169/50)
#W0142:228:MPM_Package.write: Used * or ** magic
#W0403: 28: Relative import 'rhnLog', should be 'backend.common.rhnLog'
#W0603: 72:initLOG: Using the global statement
# for pylint-1.0 we also disable
#C1001: 46, 0: Old-style class defined. (old-style-class)
#W0121: 33,16: Use raise ErrorClass(args) instead of raise ErrorClass, args. (old-raise-syntax)
#W:243, 8: Else clause on loop without a break statement (useless-else-on-loop)
# pylint-1.1 checks
#C:334, 0: No space allowed after bracket (bad-whitespace)
#W:162, 8: Attempting to unpack a non-sequence defined at line 6 of (unpacking-non-sequence)
#C: 37, 0: Unnecessary parens after 'not' keyword (superfluous-parens)
#C:301, 0: Unnecessary parens after 'if' keyword (superfluous-parens)

[REPORTS]

# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html
output-format=parseable

# Include message's id in output
include-ids=yes

# Tells whether to display a full report or only the messages
reports=yes

# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}"

[VARIABLES]

# A regular expression matching names used for dummy variables (i.e. not used).
dummy-variables-rgx=_|dummy


[BASIC]

# Regular expression which should only match correct module names
#module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
module-rgx=([a-zA-Z_][a-zA-Z0-9_]+)$

# Regular expression which should only match correct module level names
const-rgx=(([a-zA-Z_][a-zA-Z0-9_]*)|(__.*__))$

# Regular expression which should only match correct class names
class-rgx=[a-zA-Z_][a-zA-Z0-9_]+$

# Regular expression which should only match correct function names
function-rgx=[a-z_][a-zA-Z0-9_]{,42}$

# Regular expression which should only match correct method names
method-rgx=[a-z_][a-zA-Z0-9_]{,42}$

# Regular expression which should only match correct instance attribute names
attr-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct argument names
argument-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct variable names
variable-rgx=[a-z_][a-zA-Z0-9_]{,30}$

# Regular expression which should only match correct list comprehension /
# generator expression variable names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$

# Regular expression which should only match correct class sttribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,42}|(__.*__))$

# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_

# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata

# List of builtins function names that should not be used, separated by a comma
bad-functions=apply,input


[DESIGN]

# Maximum number of arguments for function / method
max-args=10

# Maximum number of locals for function / method body
max-locals=20

# Maximum number of return / yield for function / method body
max-returns=6

# Maximum number of branch for function / method body
max-branchs=20

# Maximum number of statements in function / method body
max-statements=50

# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of attributes for a class (see R0902).
max-attributes=7

# Minimum number of public methods for a class (see R0903).
min-public-methods=1

# Maximum number of public methods for a class (see R0904).
max-public-methods=20


[CLASSES]


[FORMAT]

# Maximum number of characters on a single line.
max-line-length=120

# Maximum number of lines in a module
max-module-lines=1000

# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string='    '


[MISCELLANEOUS]

# List of note tags to take in consideration, separated by a comma.
notes=
07070100000007000081B40000000000000000000000015DA8415F0000536F000000000000000000000000000000000000001A00000000spacecmd/spacecmd.changes-------------------------------------------------------------------
Thu Oct 17 12:18:46 CEST 2019 - jgonzalez@suse.com

- version 4.0.16-1
- Java api expects content as encoded string instead of encode bytes like before (bsc#1153277)

-------------------------------------------------------------------
Tue Oct 15 11:44:32 CEST 2019 - jgonzalez@suse.com

- version 4.0.15-1
- Fix building and installing on CentOS8/RES8/RHEL8
- Check that a channel doesn't have clones before deleting it (bsc#1138454)

-------------------------------------------------------------------
Tue Aug 27 23:49:29 CEST 2019 - jgonzalez@suse.com

- version 4.0.14-1
- Fix missing runtime dependencies that made spacecmd return old versions of
  packages in some cases, even if newer ones were available (bsc#1148311)

-------------------------------------------------------------------
Thu Jun 06 15:58:50 CEST 2019 - jgonzalez@suse.com

- version 4.0.13-1
- Bugfix: referenced variable before assignment.

-------------------------------------------------------------------
Thu May 30 10:54:16 CEST 2019 - jgonzalez@suse.com

- version 4.0.12-1
- Bugfix: 'dict' object has no attribute 'iteritems' (bsc#1135881)
- Add unit tests for custominfo, snippet, scap, ssm, cryptokey and distribution

-------------------------------------------------------------------
Wed May 15 15:05:51 CEST 2019 - jgonzalez@suse.com

- version 4.0.11-1
- SPEC cleanup

-------------------------------------------------------------------
Mon Apr 22 12:05:58 CEST 2019 - jgonzalez@suse.com

- version 4.0.10-1
- add unit tests for spacecmd.api, spacecmd.activationkey and spacecmd.filepreservation
- add unit tests for spacecmd.shell
- Save SSM list on system delete and update cache (bsc#1130077, bsc#1125744)
- add makefile and pylint configuration

-------------------------------------------------------------------
Mon Mar 25 16:40:32 CET 2019 - jgonzalez@suse.com

- version 4.0.9-1
- Add Pylint setup
- Replace iteritems with items for python2/3 compat (bsc#1129243)

-------------------------------------------------------------------
Mon Mar 04 09:53:28 CET 2019 - jgonzalez@suse.com

- version 4.0.8-1
- fix python 3 bytes issue when handling config channels

-------------------------------------------------------------------
Sat Mar 02 00:09:22 CET 2019 - jgonzalez@suse.com

- version 4.0.7-1
- Add '--force', '-f' option to regenerateYumCache (bsc#1127389)

-------------------------------------------------------------------
Wed Feb 27 12:59:05 CET 2019 - jgonzalez@suse.com

- version 4.0.6-1
- Prevent spacecmd crashing when piping the output in Python 3 (bsc#1125610)

-------------------------------------------------------------------
Thu Jan 31 16:34:26 CET 2019 - jgonzalez@suse.com

- version 4.0.5-1
- Fix compatibility with Python 3

-------------------------------------------------------------------
Wed Jan 16 12:16:54 CET 2019 - jgonzalez@suse.com

- version 4.0.4-1
- Fix importing state channels using configchannel_import
- Fix getting file info for latest revision (via configchannel_filedetails)

-------------------------------------------------------------------
Mon Dec 17 14:33:04 CET 2018 - jgonzalez@suse.com

- version 4.0.3-1
- Add function to merge errata and packages through spacecmd (bsc#987798)
- show group id on group_details (bsc#1111542)
- State channels handling: Existing commands configchannel_create and configchannel_import were updated
  while system_scheduleapplyconfigchannels and configchannel_updateinitsls were added.

-------------------------------------------------------------------
Fri Oct 26 10:03:04 CEST 2018 - jgonzalez@suse.com

- version 4.0.2-1
- add summary to softwarechannel.clone when calling older API versions
  (bsc#1109023)
- New function/Update old functions to handle state channels as well

-------------------------------------------------------------------
Fri Aug 10 15:10:30 CEST 2018 - jgonzalez@suse.com

- version 4.0.1-1
- Bump version to 4.0.0 (bsc#1104034)
- Fix copyright for the package specfile (bsc#1103696)
- Suggest not to use password option for spacecmd (bsc#1103090)

-------------------------------------------------------------------
Wed May 23 09:00:54 CEST 2018 - jgonzalez@suse.com

- version 2.8.25.4-1
- add option to set cleanup type for system_delete (bsc#1094190)

-------------------------------------------------------------------
Wed May 16 17:20:02 CEST 2018 - jgonzalez@suse.com

- version 2.8.25.3-1
- Sync with upstream (bsc#1083294)

-------------------------------------------------------------------
Mon Apr 23 08:55:55 CEST 2018 - jgonzalez@suse.com

- version 2.8.25.2-1
- Sync with upstream (bsc#1083294)
- 1539878 - add save_cache to do_ssm_intersect
- Fix softwarechannel_listsyncschedule

-------------------------------------------------------------------
Wed Apr 04 14:29:37 CEST 2018 - jgonzalez@suse.com

- version 2.8.21.2-1
- Disable pylint for python2 and RES < 8 (bsc#1088070)

-------------------------------------------------------------------
Mon Mar 26 08:42:35 CEST 2018 - jgonzalez@suse.com

- version 2.8.21.1-1
- Sync with upstream (bsc#1083294)
- Connect to API using FQDN instead of hostname to avoid SSL
  validation problems (bsc#1085667)

-------------------------------------------------------------------
Mon Mar 05 08:41:11 CET 2018 - jgonzalez@suse.com

- version 2.8.20.1-1
- 1536484 - Command spacecmd supports utf8 name of systems
- 1484056 - updatefile and addfile are basically same calls
- 1484056 - make configchannel_addfile fully non-interactive
- 1445725 - display all checksum types, not just MD5
- remove clean section from spec (bsc#1083294)
- Added function to update software channel. Moreover, some
  refactoring has been done(bsc#1076578)

-------------------------------------------------------------------
Fri Feb 23 12:14:59 CET 2018 - jgonzalez@suse.com

- version 2.8.17.2-1
- add more python3 compatibility changes

-------------------------------------------------------------------
Fri Feb 23 10:30:03 CET 2018 - jgonzalez@suse.com

- version 2.8.17.1-1
- Compatibility with Python 3
- Fix typo (bsc#1081151)
- Configure gpg_flag via spacecmd creating a channel (bsc#1080290)

-------------------------------------------------------------------
Mon Feb 05 12:44:39 CET 2018 - jgonzalez@suse.com

- version 2.8.15.3-1
- Allow scheduling the change of software channels as an action.
  The previous channels remain accessible to the registered system
  until the action is executed.
  to the registered system until the action is executed.

-------------------------------------------------------------------
Fri Feb 02 11:58:16 CET 2018 - jgonzalez@suse.com

- version 2.8.15.2-1
- support multiple FQDNs per system (bsc#1063419)

-------------------------------------------------------------------
Wed Jan 17 17:34:42 CET 2018 - jgonzalez@suse.com

- version 2.8.13.2-1
- Fix bsc number for change 'configchannel export binary flag to
  json'

-------------------------------------------------------------------
Wed Jan 17 11:14:54 CET 2018 - jgonzalez@suse.com

- version 2.8.13.1-1
- add --config option to spacecmd
- Added custom JSON encoder in order to parse date fields correctly (bsc#1070372)

-------------------------------------------------------------------
Fri Nov 10 16:28:48 CET 2017 - mc@suse.de

- version 2.8.10.1-1
- pylint - fix intendation

-------------------------------------------------------------------
Thu Oct 26 17:00:51 CEST 2017 - mc@suse.de

- version 2.8.9.1-1
- fix build with python 3
- show list of arches for channel
- allow softwarechannel_setsyncschedule to disable schedule
- add softwarechannel_setsyncschedule --latest
- in case of system named by id, let id take precedence
- Make spacecmd prompt for password when overriding config file user
- show less output of common packages in selected channels
- adding softwarechannel_listmanageablechannels

-------------------------------------------------------------------
Wed Aug 30 16:05:32 CEST 2017 - mc@suse.de

- version 2.7.8.7-1
- Switched logging from warning to debug

-------------------------------------------------------------------
Tue Aug 08 11:06:21 CEST 2017 - fkobzik@suse.de

- version 2.7.8.6-1
- configchannel export binary flag to json (bsc#1044719)

-------------------------------------------------------------------
Mon Jun 12 09:10:45 CEST 2017 - mc@suse.de

- version 2.7.8.5-1
- spacecmd report_outofdatesystems: avoid one XMLRPC call per system
  (bsc#1015882)

-------------------------------------------------------------------
Mon May 29 15:38:31 CEST 2017 - mc@suse.de

- version 2.7.8.4-1
- Remove debug logging from softwarechannel_sync function

-------------------------------------------------------------------
Tue May 23 07:59:56 CEST 2017 - mc@suse.de

- version 2.7.8.3-1
- Remove get_certificateexpiration support in spacecmd (bsc#1013876)

-------------------------------------------------------------------
Wed May 03 15:58:13 CEST 2017 - michele.bologna@suse.com

- version 2.7.8.2-1
- Adding softwarechannel_listmanageablechannels

-------------------------------------------------------------------
Mon Apr 03 14:55:20 CEST 2017 - mc@suse.de

- version 2.7.8.1-1
- fix syntax error

-------------------------------------------------------------------
Fri Mar 31 09:53:45 CEST 2017 - mc@suse.de

- version 2.7.7.1-1
- make sure to know if we get into default function and exit
  accordingly

-------------------------------------------------------------------
Tue Mar 07 15:13:15 CET 2017 - mc@suse.de

- version 2.7.6.1-1
- exit with 1 with incorrect command, wrong server, etc.
- Updated links to github in spec files
- print also systemdid with system name
- improve output on error for listrepo (bsc#1027426)
- print profile_name instead of string we're searching for
- Fix: reword spacecmd removal msg (bsc#1024406)
- Fix interactive mode
- Add a type parameter to repo_create

-------------------------------------------------------------------
Tue Feb 07 15:11:39 CET 2017 - michele.bologna@suse.com

- version 2.7.3.2-1
- Removed obsolete code (bsc#1013938)

-------------------------------------------------------------------
Wed Jan 11 15:44:53 CET 2017 - michele.bologna@suse.com

- version 2.7.3.1-1
- Version 2.7.3-1

-------------------------------------------------------------------
Mon Nov 07 11:30:38 CET 2016 - michele.bologna@suse.com

- version 2.5.5.3-1
- Make exception class more generic and code fixup (bsc#1003449)
- Handle exceptions raised by listChannels (bsc#1003449)
- Alert if a non-unique package ID is detected

-------------------------------------------------------------------
Tue May 24 14:46:06 CEST 2016 - kwalter@suse.com

- version 2.5.5.2-1
- make spacecmd createRepo compatible with SUSE Manager 2.1 API
  (bsc#977264)

-------------------------------------------------------------------
Wed Feb 10 08:40:17 CET 2016 - mc@suse.de

- version 2.5.5.1-1
- mimetype detection to set the binary flag requires 'file' tool
- Text description missing for remote command by Spacecmd

-------------------------------------------------------------------
Tue Jan 26 14:02:05 CET 2016 - mc@suse.de

- version 2.5.2.1-1
- spacecmd: repo_details show 'None' if repository doesn't have SSL
  Certtificate
- spacecmd: Added functions to add/edit SSL certificates for
  repositories

-------------------------------------------------------------------
Tue Jan 05 15:54:16 CET 2016 - mc@suse.de

- version 2.5.1.2-1
- build spacecmd noarch only on new systems

-------------------------------------------------------------------
Mon Nov 30 11:02:34 CET 2015 - mc@suse.de

- version 2.5.1.1-1
- mimetype detection to set the binary flag requires 'file' tool
- fix export/cloning: always base64
- Always base64 encode to avoid trim() bugs in the XML-RPC library.

-------------------------------------------------------------------
Thu Nov 19 14:07:08 UTC 2015 - dmacvicar@suse.de

- set binary mode on uploaded files based on content
  (bsc#948245)

-------------------------------------------------------------------
Wed Oct 07 14:26:44 CEST 2015 - mc@suse.de

- version 2.5.0.1-1
- drop monitoring
- replace upstream subscription counting with new subscription
  matching (FATE#311619)

-------------------------------------------------------------------
Wed Sep 23 15:05:03 CEST 2015 - mc@suse.de

- version 2.1.25.10-1
- Revert "1207606 - do not return one package multiple times" (bsc#945380)
- check for existence of device description in spacecmd system_listhardware
  (bsc#932288)

-------------------------------------------------------------------
Mon Jun 22 15:57:11 CEST 2015 - jrenner@suse.de

- version 2.1.25.9-1
- do not escape spacecmd command arguments
- do not return one package multiple times
- add system_setcontactmethod (FATE#314858)
- add activationkey_setcontactmethod (FATE#314858)
- show contact method with activationkey_details and system_details
- clone config files without loosing trailing new lines (bsc#926318)

-------------------------------------------------------------------
Tue Mar 31 14:38:08 CEST 2015 - mc@suse.de

- version 2.1.25.8-1
- sanitize data from export

-------------------------------------------------------------------
Thu Jan 29 15:51:44 CET 2015 - mc@suse.de

- version 2.1.25.7-1
- fix configchannel export - do not create 'contents' key for directories
  (bsc#908849)
- fix patch summary printing
- code cleanup
- add new function kickstart_getsoftwaredetails
- Added feature to get installed packageversion of a system or systems managed
  by ssm to spacecmd

-------------------------------------------------------------------
Thu Dec 04 13:27:13 CET 2014 - mc@suse.de

- version 2.1.25.6-1
- call listAutoinstallableChannels() for listing distributions
  (bsc#887879)
- Fix spacecmd schedule listing (bsc#902494)
- Teach spacecmd report_errata to process all-errata in the absence
  of further args

-------------------------------------------------------------------
Tue Dec  2 15:13:52 CET 2014 - mc@suse.de

- fix call of setCustomOptions() during kickstart_importjson
  (bsc#879904)

-------------------------------------------------------------------
Fri Nov 07 13:10:33 CET 2014 - mc@suse.de

- version 2.1.25.5-1
- spacecmd: fix listupgrades [bnc#892707]

-------------------------------------------------------------------
Fri Aug 01 10:14:56 CEST 2014 - mc@suse.de

- version 2.1.25.4-1
- make print_result a static method of SpacewalkShell (bnc#889605)

-------------------------------------------------------------------
Tue Jun 17 10:29:00 CEST 2014 - jrenner@suse.de

- version 2.1.25.3-1
- Added option to force deployment of a config channel to all subscribed systems
- Added last boot message in system_details command
- Updated kickstart_import documentation
- Added kickstart_import_raw command

-------------------------------------------------------------------
Tue May 06 15:14:55 CEST 2014 - mc@suse.de

- version 2.1.25.2-1
- set output encoding when stdout is not a tty

-------------------------------------------------------------------
Thu Feb 27 15:35:56 CET 2014 - fcastelli@suse.com

- version 2.1.25.1-1
- make file_needs_b64_enc work for both str and unicode inputs

-------------------------------------------------------------------
Thu Feb 13 15:34:44 CET 2014 - mc@suse.de

- version 2.1.24.1-1
- Updating the copyright years info

-------------------------------------------------------------------
Mon Jan 13 09:42:30 CET 2014 - mc@suse.de

- version 2.1.22.1-1
- fix spacecmd, so it does not expect package id within the
  system.listPackages API call
- fix binary file detection
- added function package_listdependencies

-------------------------------------------------------------------
Wed Dec 18 13:51:21 CET 2013 - mc@suse.de

- version 2.1.20.1-1
- don't attempt to write out 'None'
- fix system listing when identified by system id

-------------------------------------------------------------------
Mon Dec 09 16:42:53 CET 2013 - mc@suse.de

- version 2.1.18.1-1
- switch to 2.1

-------------------------------------------------------------------
Wed Aug 21 15:54:06 CEST 2013 - mc@suse.de

- version 1.7.7.11-1
- fixing spacecmd ssm 'list' has no attribute 'keys' error

-------------------------------------------------------------------
Wed Jun 12 13:37:52 CEST 2013 - mc@suse.de

- version 1.7.7.10-1
- spacecmd errors out when trying to add script to kickstart
- Make spacecmd able to specify config channel label

-------------------------------------------------------------------
Thu Apr 04 15:29:13 CEST 2013 - mc@suse.de

- version 1.7.7.9-1
- fix directory export in configchannel_export
- use 755 as default permissions for directories in
  configfile_getinfo
- fix directory creation in configchannel_addfile
- print the list of systems in system_runscript
- print the list of systems in system_reboot
- return a unique set from expand_systems
- print a clearer error message when duplicate system names are found
- standardize the behavior for when a system ID is not returned
- add a delay before regenerating the system cache after a delete
- handle binary files correctly in configfile_getinfo
- print the name in the confirmation message of snippet_create
- don't reuse variable names in parse_arguments
- print the function's help message when -h in the argument list
- print file path in package_details
- fixing broken export of configchannels with symlinks

-------------------------------------------------------------------
Fri Sep 28 16:15:30 CEST 2012 - mc@suse.de

- version 1.7.7.8-1
- prevent outputting escape sequences to non-terminals
- Fixed small typo in spacecmd/src/lib/kickstart.py
- do not quote argument of the help command (bnc#776615)

-------------------------------------------------------------------
Mon Jul 16 15:27:39 CEST 2012 - ug@suse.de

- version 1.7.7.7-1
- Fix kickstart_export with old API versions

-------------------------------------------------------------------
Fri Jul  6 09:46:42 CEST 2012 - ug@suse.de

- command line parameter for "distribution path" was
  documented wrong in help text
  (bnc#769106)

-------------------------------------------------------------------
Thu Jul  5 11:34:41 CEST 2012 - ug@suse.de

- "suse" was missing in the helptext of the CLI for
  distributions (bnc#769108)

-------------------------------------------------------------------
Mon Jun 25 10:23:03 CEST 2012 - mc@suse.de

- version 1.7.7.6-1
- enhancement add configchannel_sync
- enhancement add softwarechannel_sync

-------------------------------------------------------------------
Thu Jun 21 11:19:29 CEST 2012 - jrenner@suse.de

- version 1.7.7.5-1
- fixing chroot option for addscript

-------------------------------------------------------------------
Thu May 31 10:53:52 CEST 2012 - mc@suse.de

- version 1.7.7.4-1
- kickstart_getcontents fix character encoding error
- activationkey_import don't add empty package/group lists
- fix activationkey_import when no base-channel specified
- Fix reference to non-existent variable
- improve configchannel_export operation on old API versions
- *diff functions allow python 2.4 compatibility
- changed get_string_diff_dicts to better fitting replacement method
- remove reference to stage function
- add do_SPACEWALKCOMPONENT_diff functions
- system_comparewithchannel filter system packagelist
- argument validation needed for configchannel_addfile
- configchannel_addfile don't display b64 file contents

-------------------------------------------------------------------
Fri Apr 27 16:11:14 CEST 2012 - mc@suse.de

- version 1.7.7.3-1
- enhancement add system_addconfigfile
- Fix usage for configchannel_addfile
- enhancement Add system_listconfigfiles
- add option to allow templating for spacecmd kickstarting

-------------------------------------------------------------------
Fri Mar 30 15:00:21 CEST 2012 - mc@suse.de

- version 1.7.7.2-1
- softwarechannel_clone avoid ISE on duplicate name
- softwarechannel_adderrata mergeErrata should be
  cloneErrataAsOriginal
- Add globbing support to distribution_details
- Add globbing support to distribution_delete
- Cleanup some typos in comments
- custominfo_details add support for globbing key names
- custominfo_deletekey add support for globbing key names
- Add cryptokey_details globbing support
- cryptokey_delete add support for globbing
- Workaround missing date key in recent spacewalk listErrata
- Add validation to softwarechannel_adderrata channel args
- softwarechannel_adderrata add --skip mode
- Add --quick mode to softwarechannel_adderrata
- Allow config-channel export of b64 encoded files
- Update the spacecmd copyright years

-------------------------------------------------------------------
Wed Mar 21 17:47:00 CET 2012 - mc@suse.de

- version 1.7.7.1-1
- Bumping package version

-------------------------------------------------------------------
Fri Feb 11 16:24:52 CET 2011 - mantel@suse.de

- debranding

-------------------------------------------------------------------
Sun Jan 30 15:31:06 CET 2011 - mc@suse.de

- backport upstrem fixes

-------------------------------------------------------------------
Wed Sep 15 08:32:37 CEST 2010 - mantel@suse.de

- Initial release of spacecmd

-------------------------------------------------------------------
07070100000008000081B40000000000000000000000015DA8415F0000114F000000000000000000000000000000000000001700000000spacecmd/spacecmd.spec#
# spec file for package spacecmd
#
# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
# Copyright (c) 2008-2018 Red Hat, Inc.
# Copyright (c) 2011 Aron Parsons <aronparsons@gmail.com>
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
# upon. The license for this file, and modifications and additions to the
# file, is the same license as for the pristine package itself (unless the
# license for the pristine package is not an Open Source License, in which
# case the license is the MIT License). An "Open Source License" is a
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.

# Please submit bugfixes or comments via https://bugs.opensuse.org/
#


%if ! (0%{?fedora} || 0%{?rhel} > 5)
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
%{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")}
%endif

%if 0%{?fedora} || 0%{?rhel} >= 8
%{!?pylint_check: %global pylint_check 0}
%endif

%if 0%{?fedora} || 0%{?suse_version} > 1320 || 0%{?rhel} >= 8
%global build_py3   1
%global python_sitelib %{python3_sitelib}
%endif

%if 0%{?fedora} || 0%{?rhel} >= 8
%global python2prefix python2
%else
%global python2prefix python
%endif

Name:           spacecmd
Version:        4.0.16
Release:        1%{?dist}
Summary:        Command-line interface to Spacewalk and Red Hat Satellite servers
License:        GPL-3.0-or-later
Group:          Applications/System

Url:            https://github.com/uyuni-project/uyuni
Source:         https://github.com/spacewalkproject/spacewalk/archive/%{name}-%{version}.tar.gz
BuildRoot:      %{_tmppath}/%{name}-%{version}-build
%if 0%{?fedora} || 0%{?rhel} || 0%{?suse_version} >= 1210
BuildArch:      noarch
%endif

%if 0%{?pylint_check}
%if 0%{?build_py3}
BuildRequires:  spacewalk-python3-pylint
%else
BuildRequires:  spacewalk-python2-pylint
%endif
%endif
%if 0%{?build_py3}
BuildRequires:  python3
BuildRequires:  python3-devel
Requires:       python3-rpm
Requires:       python3-simplejson
Requires:       python3
%else
BuildRequires:  %{python2prefix}
BuildRequires:  %{python2prefix}-devel
Requires:       %{python2prefix}-simplejson
Requires:       rpm-python
Requires:       %{python2prefix}
%if 0%{?suse_version}
BuildRequires:  python-xml
Requires:       python-xml
%endif
%endif
%if 0%{?rhel} == 5
BuildRequires:  python-json
%endif

%if 0%{?rhel} == 5
Requires:       python-simplejson
%endif
Requires:       file

%description
spacecmd is a command-line interface to Spacewalk and Red Hat Satellite servers

%prep
%setup -q

%build
# nothing to build

%install
%{__mkdir_p} %{buildroot}/%{_bindir}

%if 0%{?build_py3}
    sed -i 's|#!/usr/bin/python|#!/usr/bin/python3|' ./src/bin/spacecmd
%endif
%{__install} -p -m0755 src/bin/spacecmd %{buildroot}/%{_bindir}/

%{__mkdir_p} %{buildroot}/%{_sysconfdir}
touch %{buildroot}/%{_sysconfdir}/spacecmd.conf

%{__mkdir_p} %{buildroot}/%{_sysconfdir}/bash_completion.d
%{__install} -p -m0644 src/misc/spacecmd-bash-completion %{buildroot}/%{_sysconfdir}/bash_completion.d/spacecmd

%{__mkdir_p} %{buildroot}/%{python_sitelib}/spacecmd
%{__install} -p -m0644 src/spacecmd/*.py %{buildroot}/%{python_sitelib}/spacecmd/

%{__mkdir_p} %{buildroot}/%{_mandir}/man1
%{__gzip} -c src/doc/spacecmd.1 > %{buildroot}/%{_mandir}/man1/spacecmd.1.gz

touch %{buildroot}/%{python_sitelib}/spacecmd/__init__.py
%{__chmod} 0644 %{buildroot}/%{python_sitelib}/spacecmd/__init__.py

%if 0%{?suse_version}
%if 0%{?build_py3}
%py3_compile -O %{buildroot}/%{python_sitelib}
%else
%py_compile -O %{buildroot}/%{python_sitelib}
%endif
%endif

%check
%if 0%{?pylint_check}
%if 0%{?build_py3}
PYTHONPATH=$RPM_BUILD_ROOT%{python_sitelib} \
	  spacewalk-python3-pylint $RPM_BUILD_ROOT%{python_sitelib}/spacecmd
%else
PYTHONPATH=$RPM_BUILD_ROOT%{python_sitelib} \
	  spacewalk-python2-pylint $RPM_BUILD_ROOT%{python_sitelib}/spacecmd
%endif
%endif

%files
%defattr(-,root,root)
%{_bindir}/spacecmd
%{python_sitelib}/spacecmd/
%ghost %config %{_sysconfdir}/spacecmd.conf
%dir %{_sysconfdir}/bash_completion.d
%{_sysconfdir}/bash_completion.d/spacecmd
%doc src/doc/README src/doc/COPYING
%doc %{_mandir}/man1/spacecmd.1.gz

%changelog
07070100000009000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000000D00000000spacecmd/src0707010000000A000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000001100000000spacecmd/src/bin0707010000000B000081FD0000000000000000000000015DA8415F00001EBC000000000000000000000000000000000000001A00000000spacecmd/src/bin/spacecmd#!/usr/bin/python
#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

""" spacecmd - a command line interface to Spacewalk """

import logging
import os
import re
import sys
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
import codecs
import locale
import argparse
try: # python 3
    from configparser import SafeConfigParser
except ImportError: # python 2
    from ConfigParser import SafeConfigParser
from socket import getfqdn

_INTRO = '''Welcome to spacecmd, a command-line interface to Spacewalk.

Type: 'help' for a list of commands
      'help <cmd>' for command-specific help
      'quit' to quit
'''

_SYSTEM_CONF_FILE = '/etc/spacecmd.conf'

if __name__ == '__main__':
    # disable no-member error message
    # pylint: disable=E1101

    if not sys.stdout.isatty():
        sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout.buffer)

    usage = '%(prog)s [options] [command]'
    parser = argparse.ArgumentParser(usage=usage)
    parser.add_argument('-c', '--config',
                         help='config file to use [default: ~/.spacecmd/config]')
    parser.add_argument('-u', '--username',
                        help='use this username to connect to the server')
    parser.add_argument('-p', '--password',
                        help='use this password to connect to the server (insecure). Use config instead or spacecmd will ask.')
    parser.add_argument('-s', '--server',
                        help='connect to this server [default: local hostname]')
    parser.add_argument('--nossl', action='store_true',
                        help='use HTTP instead of HTTPS')
    parser.add_argument('--nohistory', action='store_true',
                        help='do not store command history')
    parser.add_argument('-y', '--yes', action='store_true',
                        help='answer yes for all questions')
    parser.add_argument('-q', '--quiet', action='store_true',
                        help='print only error messages')
    parser.add_argument('-d', '--debug', action='count', default=0,
                        help='print debug messages (can be passed multiple times)')
    parser.add_argument('command', nargs='*',
                        help=argparse.SUPPRESS)

    options = parser.parse_args()
    if options.command:
        args = options.command
    else:
        args = []

    # determine the logging level
    if options.debug:
        level = logging.DEBUG
    elif options.quiet:
        level = logging.ERROR
    else:
        level = logging.INFO

    # configure logging
    logging.basicConfig(level=level, format='%(levelname)s: %(message)s')

    # files are loaded from ~/.spacecmd/
    conf_dir = os.path.expanduser('~/.spacecmd')
    user_conf_file = os.path.join(conf_dir, 'config')

    # server-specifics will be loaded from the configuration file later
    config = SafeConfigParser()

    # prevent readline from outputting escape sequences to non-terminals
    if not sys.stdout.isatty():
        logging.debug('stdout is not a TTY, setting TERM=dumb')
        os.environ['TERM'] = 'dumb'

    # import our Cmd subclass after we settle our TERM value
    from spacecmd.shell import SpacewalkShell, UnknownCallException

    # create an instance of the shell
    shell = SpacewalkShell(options, conf_dir, config)

    # set the default server to local hostname
    if shell.options.server:
        shell.config['server'] = shell.options.server
    else:
        shell.config['server'] = getfqdn()

    # don't automatically create config files passed via --config
    if shell.options.config:
        if not os.path.isfile(shell.options.config):
            logging.error('Config file %s does not exist.', shell.options.config)
            sys.exit(1)
    else:
    # create an empty configuration file if one's not present
        if not os.path.isfile(user_conf_file):
            try:
                # create ~/.spacecmd
                if not os.path.isdir(conf_dir):
                    logging.debug('Creating %s', conf_dir)
                    os.mkdir(conf_dir, int('0700', 8))

                # create a template configuration file
                logging.debug('Creating configuration file: %s', user_conf_file)
                handle = open(user_conf_file, 'w')
                handle.write('[spacecmd]\n')
                handle.close()
            except IOError:
                logging.error('Could not create %s', user_conf_file)

    # load options from configuration files
    if shell.options.config:
        files_read = config.read([_SYSTEM_CONF_FILE, user_conf_file, shell.options.config])
    else:
        files_read = config.read([_SYSTEM_CONF_FILE, user_conf_file])

    for item in files_read:
        logging.debug('Read configuration from %s', item)

    # load the default configuration section
    shell.load_config_section('spacecmd')

    # run a single command from the command line
    if len(args):
        try:
            # rebuild the command and quote all arguments to be safe
            # except for help command
            command = args[0]

            if command == 'help':
                command = ' '.join(args)
            if len(args) > 1:
                command += ' %s' % ' '.join('%s' % s if not True in [ c.isspace() for c in s ] else "'%s'" % s for s in args[1:])

            # run the command
            precmd = shell.precmd(command)
            if precmd == '':
                sys.exit(1)
            shell.print_result(shell.onecmd(precmd), precmd)
        except KeyboardInterrupt:
            print
            print('User Interrupt')
        except UnknownCallException:
            sys.exit(1)
        except Exception as detail:
            # get the relevant part of a XML-RPC fault
            if isinstance(detail, xmlrpclib.Fault):
                detail = detail.faultString

            if shell.options.debug:
                # print(the traceback when debugging)
                logging.exception(detail)
            else:
                logging.error(detail)

            sys.exit(1)
    else:
        if not shell.options.quiet:
            print(_INTRO)

        if not shell.do_login(''):
            sys.exit(1)

        # stay in the interactive shell forever
        while True:
            try:
                shell.cmdloop()
            except KeyboardInterrupt:
                print
            except SystemExit:
                sys.exit(0)
            except UnknownCallException:
                pass
            except Exception as detail:
                # get the relevant part of a XML-RPC fault
                if isinstance(detail, xmlrpclib.Fault):
                    detail = detail.faultString

                    # the session expired
                    if re.search('Could not find session', detail, re.I):
                        shell.session = ''

                if shell.options.debug:
                    # print(the traceback when debugging)
                    logging.exception(detail)
                else:
                    logging.error(detail)
0707010000000C000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000001100000000spacecmd/src/doc0707010000000D000081B40000000000000000000000015DA8415F0000894B000000000000000000000000000000000000001900000000spacecmd/src/doc/COPYING                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    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/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
0707010000000E000081B40000000000000000000000015DA8415F00000D04000000000000000000000000000000000000001800000000spacecmd/src/doc/READMEspacecmd - a command line interface to Spacewalk and Satellite servers

Author:   Aron Parsons <aronparsons@gmail.com>
Homepage: https://github.com/spacewalkproject/spacewalk/wiki/spacecmd

# Requirements
- >= python-2.4
- a Spacewalk or Satellite server with an API version
  of 10.8 or higher

# Highlights
- view and manage nearly every aspect of Satellite
- tab completion
- can be passed single commands (useful for shell scripts)
- can be used as an interactive shell
- advanced searching of systems allows systems to be managed
  without having to create system groups
- all functionality implemented via the Satellite API

# Examples
#
# add and remove packages from the commandline
#
[user@sat]$ spacecmd -y system_installpackage www* mod_python
Scheduled 6 system(s)

[user@sat]$ spacecmd -y system_removepackage wiki02 mod_perl
Scheduled 1 system(s)


#
# apply errata from the command line
#
[user@sat]$ spacecmd -y errata_apply RHSA-2010:0423
Scheduled 42 system(s)

[user@sat]$ spacecmd -y system_applyerrata group:web_servers RHSA-2010:0040
Scheduled 16 system(s)


#
# quickly generate reports
#
spacecmd> system_listerrata ldap03
System: ldap03

Security Errata:
RHSA-2010:0458  Moderate: perl security update                        6/7/10
RHSA-2010:0449  Moderate: rhn-client-tools security update            6/1/10
RHSA-2010:0423  Important: krb5 security update                      5/18/10

spacecmd> report_errata
# Systems       Errata
---------       ------
CLA-2010:0474       88
CLA-2010:0475        6
CLA-2010:0488      183
CLA-2010:0490      273
CLA-2010:0500        4
CLA-2010:0501        5
RHBA-2010:0402       1
RHSA-2010:0474       2
RHSA-2010:0488       1
RHSA-2010:0490       5

spacecmd> report_outofdatesystems
System        Packages
------        --------
monkey             310
shark               63
hedgehog            39
pomeranian           4

spacecmd> report_ipaddresses
System   Hostname                IP
------   --------                --
dns01    dns01.dmz.example.com   192.168.254.53
www01    www01.dmz.example.com   192.168.254.80
ztest    ztest.test.example.com  192.168.42.111

spacecmd> report_kernels
System       Kernel
------       ------
system01     2.6.9-89.0.25.ELsmp
system02     2.6.9-89.0.3.ELsmp
system03     2.6.9-89.0.26.ELsmp


#
# make temporary groups on-the-fly
#
spacecmd> ssm_add search:driver:bnx2
Systems Selected: 111

spacecmd> ssm_add search:device:vmware
Systems Selected: 285

spacecmd> ssm_add search:hostname:external.example.com
Systems Selected: 16


#
# tab completion of everything
#
spacecmd> system_installpackage ssm vmware-tools [tab]
vmware-tools         vmware-tools-kmod
vmware-tools-common  vmware-tools-nox


#
# easily view system information
#
spacecmd> system_details www01.example.com
Name:          www01.example.com
System ID:     1000010001
Locked:        False
Registered:    20100311 19:31:36
Last Checkin:  20100621 18:31:53
OSA Status:    online

Hostname:      www01.example.com
IP Address:    192.168.1.80
Kernel:        2.6.18-164.el5

Software Channels:
  custom-rhel-i386-server-5
    |-- custom-extras-i386-rhel5
    |-- clone-rhn-tools-rhel-i386-server-5

Configuration Channels:
  sudoers
  base
  base-rhel5

Entitlements:
  Management
  Provisioning

System Groups:
  all_linux_systems
  all_linux_VMs
  rhel5-i386
0707010000000F000081B40000000000000000000000015DA8415F000017BC000000000000000000000000000000000000001C00000000spacecmd/src/doc/spacecmd.1.TH "spacecmd" "1" "" "Aron Parsons" ""
.SH NAME
spacecmd \- a command-line interface to Spacewalk and Satellite servers
.SH SYNOPSIS
\fBspacecmd\fP [\fIoptions\fP] [\fIcommand\fP]
.SH OVERVIEW
.nf
spacecmd is a command-line interface to Spacewalk and Satellite servers.
It is written in Python and uses the XML-RPC API provided by the server.
Nearly all aspects of the Satellite can be managed by spacecmd.  These
include managing systems, software channels, configuration channels,
activation keys, Kickstarts and users.

spacecmd can be run as an interactive shell by running it without a
command.  It can also run a single command by passing the command
and the required arguments on the command line.

Run 'spacecmd help' to list the actions that are availble.

Run 'spacecmd -- <command> --help' to get help for a particular command.
.fi
.SH RUNNING SINGLE COMMANDS
.nf
When running spacecmd non-interactively, you must take care to escape
arguments passed to the spacecmd functions.  This involves putting a '--'
before the command starts so that the arguments to the function are not
treated as global arguments to spacecmd.  You must also escape any quotes
that you pass to the functions so that the shell does not interpret them.
.P
.B Example:
.nf
spacecmd -s server1 -- softwarechannel_create -n \\'My Channel\\' \\
                                              -l channel1 \\
                                              -a x86_64
.fi
.SH OPTIONS
.TP
.B \-c CONFIG_FILE, \-\-config=CONFIG_FILE
config file to use [default: ~/.spacecmd/config]
.TP
.B \-u USERNAME, \-\-username=USERNAME
use this username to connect to the server
.TP
.B \-p PASSWORD, \-\-password=PASSWORD
use this password to connect to the server (insecure).
.nf
This option is insecure because process listing tools like ps will
show it as part of command line visible for all user.
If no password is provided, spacecmd will try to read it from the
config file or ask for it.
.fi
.TP
.B \-s SERVER, \-\-server=SERVER
connect to this server [default: localhost]
.TP
.B \-\-nossl
use HTTP instead of HTTPS
.TP
.B \-\-nohistory
do not store command history
.TP
.B \-y, \-\-yes
answer yes for all questions
.TP
.B \-q, \-\-quiet
print only error messages
.TP
.B \-d, \-\-debug
print debug messages
.TP
.B \-h, \-\-help
show this help message and exit
.fi
.SH CONFIGURATION FILES
.nf
Configuration files are loaded from /etc/spacecmd.conf and
~/.spacecmd/config, unless overridden by the --config option.  The
default section is [spacecmd].  It can then be overridden with server
specific sections.  Command-line arguments always override options in
the configuration files.
.fi
.P
.nf
[spacecmd]
server=localhost
username=admin
password=redhat
nossl=0

[satellite.example.com]
username=joe
password=secret
nossl=1
.fi
.SH EXAMPLES
.P
.B Make temporary groups on-the-fly
.nf
spacecmd> ssm_add search:driver:bnx2
Systems Selected: 111

spacecmd> ssm_add search:device:vmware
Systems Selected: 285

spacecmd> ssm_add search:hostname:external.example.com
Systems Selected: 16
.fi

.P
.B Add and remove packages from the commandline
.nf
[user@sat]$ spacecmd -y system_installpackage www* mod_python
Scheduled 6 system(s)

[user@sat]$ spacecmd -y system_removepackage wiki02 mod_perl
Scheduled 1 system(s)
.fi

.P
.B Schedule reboots from the commandline
.nf
[user@sat]$ spacecmd -y -- system_reboot ldap* -s +6h

Start Time: 20160106T07:01:00

Systems
-------
ldap01
ldap02
ldap03

[user@sat]$ spacecmd -y system_reboot www01

Start Time: 20160106T02:01:00

Systems
-------
www01

.fi

.P
.B Apply errata from the command line
.nf
[user@sat]$ spacecmd -y errata_apply RHSA-2010:0423
Scheduled 42 system(s)

[user@sat]$ spacecmd -y system_applyerrata group:web_servers RHSA-2010:0040
Scheduled 16 system(s)
.fi

.P
.B Quickly generate reports
.nf
spacecmd> system_listerrata ldap03
System: ldap03

Security Errata:
RHSA-2010:0458  Moderate: perl security update                        6/7/10
RHSA-2010:0449  Moderate: rhn-client-tools security update            6/1/10
RHSA-2010:0423  Important: krb5 security update                      5/18/10

spacecmd> report_errata
# Systems       Errata
---------       ------
CLA-2010:0474       88
CLA-2010:0475        6
CLA-2010:0488      183
CLA-2010:0490      273
CLA-2010:0500        4
CLA-2010:0501        5
RHBA-2010:0402       1
RHSA-2010:0474       2
RHSA-2010:0488       1
RHSA-2010:0490       5

spacecmd> report_outofdatesystems
System        Packages
------        --------
monkey             310
shark               63
hedgehog            39
pomeranian           4

spacecmd> report_ipaddresses
System   Hostname                IP
------   --------                --
dns01    dns01.dmz.example.com   192.168.254.53
www01    www01.dmz.example.com   192.168.254.80
ztest    ztest.test.example.com  192.168.42.111

spacecmd> report_kernels
System       Kernel
------       ------
system01     2.6.9-89.0.25.ELsmp
system02     2.6.9-89.0.3.ELsmp
system03     2.6.9-89.0.26.ELsmp
.fi

.P
.B Tab completion of everything
.nf
spacecmd> system_installpackage ssm vmware-tools [tab]
vmware-tools         vmware-tools-kmod
vmware-tools-common  vmware-tools-nox
.fi

.P
.B Easily view system information
.nf
spacecmd> system_details www01.example.com
Name:          www01.example.com
System ID:     1000010001
Locked:        False
Registered:    20100311 19:31:36
Last Checkin:  20100621 18:31:53
OSA Status:    online

Hostname:      www01.example.com
IP Address:    192.168.1.80
Kernel:        2.6.18-164.el5

Software Channels:
  custom-rhel-i386-server-5
    |-- custom-extras-i386-rhel5
    |-- clone-rhn-tools-rhel-i386-server-5

Configuration Channels:
  sudoers
  base
  base-rhel5

Entitlements:
  Management
  Provisioning

System Groups:
  all_linux_systems
  all_linux_VMs
  rhel5-i386
.nf
.SH BUGS
.nf
Please report any bugs to https://bugzilla.redhat.com/ under the "spacecmd"
component.
.nf
.SH HOMEPAGE
https://github.com/spacewalkproject/spacewalk/wiki/spacecmd
.nf
.fi
.SH AUTHOR
spacecmd was written by Aron Parsons <aronparsons@gmail.com>
07070100000010000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000001200000000spacecmd/src/misc07070100000011000081B40000000000000000000000015DA8415F000001E9000000000000000000000000000000000000002B00000000spacecmd/src/misc/spacecmd-bash-completion_spacecmd()
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    opts=`for i in $(spacecmd help | tail -n +4 | head -n -5 ); do echo $i; done`

        if [[ ${cur} == * ]]; then
            if [[ ${#COMP_WORDS[@]} -gt 2 ]]; then
                return 0
            else
                COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
                return 0
            fi
        fi
}
complete -F _spacecmd spacecmd
07070100000012000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000001600000000spacecmd/src/spacecmd07070100000013000081B40000000000000000000000015DA8415F00000029000000000000000000000000000000000000002200000000spacecmd/src/spacecmd/__init__.py# coding: utf-8
"""
Package spacecmd
"""
07070100000014000081B40000000000000000000000015DA8415F0000DF3A000000000000000000000000000000000000002700000000spacecmd/src/spacecmd/activationkey.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import re
import shlex
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_activationkey_addpackages(self):
    print('activationkey_addpackages: Add packages to an activation key')
    print('usage: activationkey_addpackages KEY <PACKAGE ...>')


def complete_activationkey_addpackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True),
                             text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(), text)


def do_activationkey_addpackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_addpackages()
        return

    key = args.pop(0)
    packages = [{'name': a} for a in args]

    self.client.activationkey.addPackages(self.session, key, packages)

####################


def help_activationkey_removepackages(self):
    print('activationkey_removepackages: Remove packages from an ' +
          'activation key')
    print('usage: activationkey_removepackages KEY <PACKAGE ...>')


def complete_activationkey_removepackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True),
                             text)
    elif len(parts) > 2:
        details = self.client.activationkey.getDetails(self.session,
                                                       parts[1])
        packages = [p['name'] for p in details.get('packages')]
        return tab_completer(packages, text)


def do_activationkey_removepackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_removepackages()
        return

    key = args.pop(0)
    packages = [{'name': a} for a in args]

    self.client.activationkey.removePackages(self.session, key, packages)

####################


def help_activationkey_addgroups(self):
    print('activationkey_addgroups: Add groups to an activation key')
    print('usage: activationkey_addgroups KEY <GROUP ...>')


def complete_activationkey_addgroups(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_group_list('', True), parts[-1])


def do_activationkey_addgroups(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_addgroups()
        return

    key = args.pop(0)

    groups = []
    for a in args:
        details = self.client.systemgroup.getDetails(self.session, a)
        groups.append(details.get('id'))

    self.client.activationkey.addServerGroups(self.session, key, groups)

####################


def help_activationkey_removegroups(self):
    print('activationkey_removegroups: Remove groups from an activation key')
    print('usage: activationkey_removegroups KEY <GROUP ...>')


def complete_activationkey_removegroups(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        key_details = self.client.activationkey.getDetails(self.session,
                                                           parts[-1])

        groups = []
        for group in key_details.get('server_group_ids'):
            details = self.client.systemgroup.getDetails(self.session,
                                                         group)
            groups.append(details.get('name'))

        return tab_completer(groups, text)


def do_activationkey_removegroups(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_removegroups()
        return

    key = args.pop(0)

    groups = []
    for a in args:
        details = self.client.systemgroup.getDetails(self.session, a)
        groups.append(details.get('id'))

    self.client.activationkey.removeServerGroups(self.session, key, groups)

####################


def help_activationkey_addentitlements(self):
    print('activationkey_addentitlements: Add entitlements to an ' +
          'activation key')
    print('usage: activationkey_addentitlements KEY <ENTITLEMENT ...>')


def complete_activationkey_addentitlements(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True),
                             text)
    elif len(parts) > 2:
        return tab_completer(self.ENTITLEMENTS, text)


def do_activationkey_addentitlements(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_addentitlements()
        return

    key = args.pop(0)
    entitlements = args

    self.client.activationkey.addEntitlements(self.session,
                                              key,
                                              entitlements)

####################


def help_activationkey_removeentitlements(self):
    print('activationkey_removeentitlements: Remove entitlements from an ' +
          'activation key')
    print('usage: activationkey_removeentitlements KEY <ENTITLEMENT ...>')


def complete_activationkey_removeentitlements(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        details = \
            self.client.activationkey.getDetails(self.session, parts[1])

        entitlements = details.get('entitlements')
        return tab_completer(entitlements, text)


def do_activationkey_removeentitlements(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_removeentitlements()
        return

    key = args.pop(0)
    entitlements = args

    self.client.activationkey.removeEntitlements(self.session,
                                                 key,
                                                 entitlements)

####################


def help_activationkey_addchildchannels(self):
    print('activationkey_addchildchannels: Add child channels to an ' +
          'activation key')
    print('usage: activationkey_addchildchannels KEY <CHANNEL ...>')


def complete_activationkey_addchildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True),
                             text)
    elif len(parts) > 2:
        key_details = \
            self.client.activationkey.getDetails(self.session, parts[1])
        base_channel = key_details.get('base_channel_label')

        all_channels = \
            self.client.channel.listSoftwareChannels(self.session)

        child_channels = []
        for c in all_channels:
            if base_channel == 'none':
                # this gets all child channels
                if c.get('parent_label'):
                    child_channels.append(c.get('label'))
            else:
                if c.get('parent_label') == base_channel:
                    child_channels.append(c.get('label'))

        return tab_completer(child_channels, text)


def do_activationkey_addchildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_addchildchannels()
        return

    key = args.pop(0)
    channels = args

    self.client.activationkey.addChildChannels(self.session, key, channels)

####################


def help_activationkey_removechildchannels(self):
    print('activationkey_removechildchannels: Remove child channels from ' +
          'an activation key')
    print('usage: activationkey_removechildchannels KEY <CHANNEL ...>')


def complete_activationkey_removechildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        key_details = \
            self.client.activationkey.getDetails(self.session, parts[1])

        return tab_completer(key_details.get('child_channel_labels'), text)


def do_activationkey_removechildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_removechildchannels()
        return

    key = args.pop(0)
    channels = args

    self.client.activationkey.removeChildChannels(self.session,
                                                  key,
                                                  channels)

####################


def help_activationkey_listchildchannels(self):
    print('activationkey_listchildchannels: List the child channels ' +
          'for an activation key')
    print('usage: activationkey_listchildchannels KEY')


def complete_activationkey_listchildchannels(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listchildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listchildchannels()
        return

    key = args[0]

    details = self.client.activationkey.getDetails(self.session, key)

    if details.get('child_channel_labels'):
        print('\n'.join(sorted(details.get('child_channel_labels'))))

####################


def help_activationkey_listbasechannel(self):
    print('activationkey_listbasechannel: List the base channels ' +
          'for an activation key')
    print('usage: activationkey_listbasechannel KEY')


def complete_activationkey_listbasechannel(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listbasechannel(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listbasechannel()
        return

    key = args[0]

    details = self.client.activationkey.getDetails(self.session, key)

    print(details.get('base_channel_label'))

####################


def help_activationkey_listgroups(self):
    print('activationkey_listgroups: List the groups for an ' +
          'activation key')
    print('usage: activationkey_listgroups KEY')


def complete_activationkey_listgroups(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listgroups(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listgroups()
        return

    key = args[0]

    details = self.client.activationkey.getDetails(self.session, key)

    for group in details.get('server_group_ids'):
        group_details = self.client.systemgroup.getDetails(self.session,
                                                           group)
        print(group_details.get('name'))

####################


def help_activationkey_listentitlements(self):
    print('activationkey_listentitlements: List the entitlements ' +
          'for an activation key')
    print('usage: activationkey_listentitlements KEY')


def complete_activationkey_listentitlements(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listentitlements(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listentitlements()
        return

    key = args[0]

    details = self.client.activationkey.getDetails(self.session, key)

    if details.get('entitlements'):
        print('\n'.join(details.get('entitlements')))

####################


def help_activationkey_listpackages(self):
    print('activationkey_listpackages: List the packages for an ' +
          'activation key')
    print('usage: activationkey_listpackages KEY')


def complete_activationkey_listpackages(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listpackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listpackages()
        return

    key = args[0]

    details = self.client.activationkey.getDetails(self.session, key)

    for package in details.get('packages'):
        if 'arch' in package:
            print('%s.%s' % (package['name'], package['arch']))
        else:
            print(package['name'])

####################


def help_activationkey_listconfigchannels(self):
    print('activationkey_listconfigchannels: List the configuration ' +
          'channels for an activation key')
    print('usage: activationkey_listconfigchannels KEY')


def complete_activationkey_listconfigchannels(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listconfigchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listconfigchannels()
        return

    key = args[0]

    channels = \
        self.client.activationkey.listConfigChannels(self.session,
                                                     key)

    channels = sorted([c.get('label') for c in channels])

    if channels:
        print('\n'.join(channels))

####################


def help_activationkey_addconfigchannels(self):
    print('activationkey_addconfigchannels: Add config channels ' +
          'to an activation key')
    print('''usage: activationkey_addconfigchannels KEY <CHANNEL ...> [options])

options:
  -t add channels to the top of the list
  -b add channels to the bottom of the list''')


def complete_activationkey_addconfigchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_configchannel_list('', True), text)


def do_activationkey_addconfigchannels(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-t', '--top', action='store_true')
    arg_parser.add_argument('-b', '--bottom', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_activationkey_addconfigchannels()
        return

    key = [args.pop(0)]
    channels = args

    if is_interactive(options):
        answer = prompt_user('Add to top or bottom? [T/b]:')
        if re.match('b', answer, re.I):
            options.top = False
        else:
            options.top = True
    else:
        if options.bottom:
            options.top = False
        else:
            options.top = True

    self.client.activationkey.addConfigChannels(self.session,
                                                key,
                                                channels,
                                                options.top)

####################


def help_activationkey_removeconfigchannels(self):
    print('activationkey_removeconfigchannels: Remove config channels ' +
          'from an activation key')
    print('usage: activationkey_removeconfigchannels KEY <CHANNEL ...>')


def complete_activationkey_removeconfigchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        key_channels = \
            self.client.activationkey.listConfigChannels(self.session,
                                                         parts[1])

        config_channels = [c.get('label') for c in key_channels]
        return tab_completer(config_channels, text)


def do_activationkey_removeconfigchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_removeconfigchannels()
        return

    key = [args.pop(0)]
    channels = args

    self.client.activationkey.removeConfigChannels(self.session,
                                                   key,
                                                   channels)

####################


def help_activationkey_setconfigchannelorder(self):
    print('activationkey_setconfigchannelorder: Set the ranked order of ' +
          'configuration channels')
    print('usage: activationkey_setconfigchannelorder KEY')


def complete_activationkey_setconfigchannelorder(self, text, line, beg,
                                                 end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_setconfigchannelorder(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_activationkey_setconfigchannelorder()
        return

    key = args[0]

    # get the current configuration channels from the first activationkey
    # in the list
    new_channels = \
        self.client.activationkey.listConfigChannels(self.session, key)
    new_channels = [c.get('label') for c in new_channels]

    # call an interface for the user to make selections
    all_channels = self.do_configchannel_list('', True)
    new_channels = config_channel_order(all_channels, new_channels)

    print('')
    print('New Configuration Channels:')
    for i, new_channel in enumerate(new_channels, 1):
        print('[%i] %s' % (i, new_channel))

    self.client.activationkey.setConfigChannels(self.session,
                                                [key],
                                                new_channels)

####################


def help_activationkey_create(self):
    print('activationkey_create: Create an activation key')
    print('''usage: activationkey_create [options])

options:
  -n NAME
  -d DESCRIPTION
  -b BASE_CHANNEL
  -u set key as universal default
  -e [enterprise_entitled,virtualization_host]''')


def do_activationkey_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-d', '--description')
    arg_parser.add_argument('-b', '--base-channel')
    arg_parser.add_argument('-e', '--entitlements')
    arg_parser.add_argument('-u', '--universal', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name (blank to autogenerate):')
        options.description = prompt_user('Description [None]:')

        print('')
        print('Base Channels')
        print('-------------')
        print('\n'.join(sorted(self.list_base_channels())))
        print('')

        options.base_channel = prompt_user('Base Channel (blank for default):')

        options.entitlements = []

        for e in self.ENTITLEMENTS:
            if e == 'enterprise_entitled':
                continue

            if self.user_confirm('%s Entitlement [y/N]:' % e,
                                 ignore_yes=True):
                options.entitlements.append(e)

        options.universal = self.user_confirm('Universal Default [y/N]:',
                                              ignore_yes=True)
    else:
        if not options.name:
            options.name = ''
        if not options.description:
            options.description = ''
        if not options.base_channel:
            options.base_channel = ''
        if not options.universal:
            options.universal = False
        if options.entitlements:
            options.entitlements = options.entitlements.split(',')

            # remove empty strings from the list
            if '' in options.entitlements:
                options.entitlements.remove('')
        else:
            options.entitlements = []

    new_key = self.client.activationkey.create(self.session,
                                               options.name,
                                               options.description,
                                               options.base_channel,
                                               options.entitlements,
                                               options.universal)

    logging.info('Created activation key %s' % new_key)

####################


def help_activationkey_delete(self):
    print('activationkey_delete: Delete an activation key')
    print('usage: activationkey_delete KEY')


def complete_activationkey_delete(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_delete()
        return

    # allow globbing of activationkey names
    keys = filter_results(self.do_activationkey_list('', True), args)
    logging.debug("activationkey_delete called with args %s, keys=%s" %
                  (args, keys))

    if not keys:
        logging.error("No keys matched argument %s" % args)
        return

    # Print the keys prior to the confimation
    print('\n'.join(sorted(keys)))

    if not self.user_confirm('Delete activation key(s) [y/N]:'):
        return

    for key in keys:
        logging.debug("Deleting key %s" % key)
        self.client.activationkey.delete(self.session, key)

####################


def help_activationkey_list(self):
    print('activationkey_list: List all activation keys')
    print('usage: activationkey_list')


def do_activationkey_list(self, args, doreturn=False):
    all_keys = self.client.activationkey.listActivationKeys(self.session)

    keys = []
    for k in all_keys:
        # don't list auto-generated re-activation keys
        if not re.match('Kickstart re-activation', k.get('description')):
            keys.append(k.get('key'))

    if doreturn:
        return keys
    else:
        if keys:
            print('\n'.join(sorted(keys)))

####################


def help_activationkey_listsystems(self):
    print('activationkey_listsystems: List systems registered with a key')
    print('usage: activationkey_listsystems KEY')


def complete_activationkey_listsystems(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_listsystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_listsystems()
        return

    key = args[0]

    try:
        systems = \
            self.client.activationkey.listActivatedSystems(self.session,
                                                           key)
    except xmlrpclib.Fault:
        logging.warning('%s is not a valid activation key' % key)
        return

    systems = sorted([s.get('hostname') for s in systems])

    if systems:
        print('\n'.join(systems))

####################


def help_activationkey_details(self):
    print('activationkey_details: Show the details of an activation key')
    print('usage: activationkey_details KEY ...')


def complete_activationkey_details(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_details()
        return

    add_separator = False

    result = []
    for key in args:
        try:
            details = self.client.activationkey.getDetails(self.session,
                                                           key)
            config_channels = \
                self.client.activationkey.listConfigChannels(
                    self.session, key)

            config_channel_deploy = \
                self.client.activationkey.checkConfigDeployment(
                    self.session, key)

            # API returns 0/1 instead of boolean
            config_channel_deploy = config_channel_deploy == 1
        except xmlrpclib.Fault:
            logging.warning('%s is not a valid activation key' % key)
            return

        groups = []
        for group in details.get('server_group_ids'):
            group_details = self.client.systemgroup.getDetails(self.session,
                                                               group)
            groups.append(group_details.get('name'))

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        result.append('Key:                    %s' % details.get('key'))
        result.append('Description:            %s' % details.get('description'))
        result.append('Universal Default:      %s' % details.get('universal_default'))
        result.append('Usage Limit:            %s' % details.get('usage_limit'))
        result.append('Deploy Config Channels: %s' % config_channel_deploy)
        if 'contact_method' in details:
            result.append('Contact Method:         %s' % details.get('contact_method'))

        result.append('')
        result.append('Software Channels')
        result.append('-----------------')
        result.append(details.get('base_channel_label'))

        for channel in sorted(details.get('child_channel_labels')):
            result.append(' |-- %s' % channel)

        result.append('')
        result.append('Configuration Channels')
        result.append('----------------------')
        for channel in config_channels:
            result.append(channel.get('label'))

        result.append('')
        result.append('Entitlements')
        result.append('------------')
        result.append('\n'.join(sorted(details.get('entitlements'))))

        result.append('')
        result.append('System Groups')
        result.append('-------------')
        result.append('\n'.join(sorted(groups)))

        result.append('')
        result.append('Packages')
        result.append('--------')
        for package in sorted(details.get('packages')):
            name = package.get('name')

            if package.get('arch'):
                name += '.%s' % package.get('arch')

            result.append(name)
    return result

####################


def help_activationkey_enableconfigdeployment(self):
    print('activationkey_enableconfigdeployment: Enable config ' +
          'channel deployment')
    print('usage: activationkey_enableconfigdeployment KEY')


def complete_activationkey_enableconfigdeployment(self, text, line, beg,
                                                  end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_enableconfigdeployment(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_enableconfigdeployment()
        return

    for key in args:
        logging.debug('Enabling config file deployment for %s' % key)
        self.client.activationkey.enableConfigDeployment(self.session, key)

####################


def help_activationkey_disableconfigdeployment(self):
    print('activationkey_disableconfigdeployment: Disable config ' +
          'channel deployment')
    print('usage: activationkey_disableconfigdeployment KEY')


def complete_activationkey_disableconfigdeployment(self, text, line, beg,
                                                   end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_disableconfigdeployment(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_disableconfigdeployment()
        return

    for key in args:
        logging.debug('Disabling config file deployment for %s' % key)
        self.client.activationkey.disableConfigDeployment(self.session, key)

####################


def help_activationkey_setbasechannel(self):
    print('activationkey_setbasechannel: Set the base channel of an ' +
          'activation key')
    print('usage: activationkey_setbasechannel KEY CHANNEL')


def complete_activationkey_setbasechannel(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.list_base_channels(), text)


def do_activationkey_setbasechannel(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_setbasechannel()
        return

    key = args.pop(0)
    channel = args[0]

    current_details = self.client.activationkey.getDetails(self.session,
                                                           key)

    details = {'description': current_details.get('description'),
               'base_channel_label': channel,
               'usage_limit': current_details.get('usage_limit'),
               'universal_default':
               current_details.get('universal_default')}

    # getDetails returns a usage_limit of 0 unlimited, which is then
    # interpreted literally as zero when passed into setDetails, doh!
    # Setting it to -1 seems to keep the usage limit unlimited
    if details['usage_limit'] == 0:
        details['usage_limit'] = -1

    self.client.activationkey.setDetails(self.session, key, details)

####################


def help_activationkey_setusagelimit(self):
    print('activationkey_setusagelimit: Set the usage limit of an ' +
          'activation key, can be a number or \"unlimited\"')
    print('usage: activationkey_setusagelimit KEY <usage limit>')
    print('usage: activationkey_setusagelimit KEY unlimited ')


def complete_activationkey_setusagelimit(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    elif len(parts) > 2:
        return "unlimited"


def do_activationkey_setusagelimit(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_setusagelimit()
        return

    key = args.pop(0)
    usage_limit = -1
    if args[0] == 'unlimited':
        logging.debug("Setting usage for key %s unlimited" % key)
    else:
        try:
            usage_limit = int(args[0])
            logging.debug("Setting usage for key %s to %d" % (key, usage_limit))
        except ValueError:
            logging.error("Couldn't convert argument %s to an integer" %
                          args[0])
            self.help_activationkey_setusagelimit()
            return

    current_details = self.client.activationkey.getDetails(self.session,
                                                           key)
    details = {'description': current_details.get('description'),
               'base_channel_label':
               current_details.get('base_channel_label'),
               'usage_limit': usage_limit,
               'universal_default':
               current_details.get('universal_default')}

    self.client.activationkey.setDetails(self.session, key, details)

####################


def help_activationkey_setuniversaldefault(self):
    print('activationkey_setuniversaldefault: Set this key as the ' +
          'universal default')
    print('usage: activationkey_setuniversaldefault KEY')


def complete_activationkey_setuniversaldefault(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_setuniversaldefault(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_activationkey_setuniversaldefault()
        return

    key = args.pop(0)

    current_details = self.client.activationkey.getDetails(self.session,
                                                           key)

    details = {'description': current_details.get('description'),
               'base_channel_label':
               current_details.get('base_channel_label'),
               'usage_limit': current_details.get('usage_limit'),
               'universal_default': True}

    # getDetails returns a usage_limit of 0 unlimited, which is then
    # interpreted literally as zero when passed into setDetails, doh!
    # Setting it to -1 seems to keep the usage limit unlimited
    if details['usage_limit'] == 0:
        details['usage_limit'] = -1

    self.client.activationkey.setDetails(self.session, key, details)

####################


def help_activationkey_export(self):
    print('activationkey_export: Export activation key(s) to JSON format file')
    print('''usage: activationkey_export [options] [<KEY> ...])

options:
    -f outfile.json : specify an output filename, defaults to <KEY>.json
                      if exporting a single key, akeys.json for multiple keys,
                      or akey_all.json if no KEY specified (export ALL)

Note : KEY list is optional, default is to export ALL keys ''')


def complete_activationkey_export(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def export_activationkey_getdetails(self, key):
    # Get the key details
    logging.info("Getting activation key details for %s" % key)
    details = self.client.activationkey.getDetails(self.session, key)

    # Get the key config-channel data, add it to the existing details
    logging.debug("activationkey.listConfigChannels %s" % key)
    ccdlist = []
    try:
        ccdlist = self.client.activationkey.listConfigChannels(self.session,
                                                               key)
    except xmlrpclib.Fault:
        logging.debug("activationkey.listConfigChannel threw an exeception, setting config_channels=False")

    cclist = [c['label'] for c in ccdlist]
    logging.debug("Got config channel label list of %s" % cclist)
    details['config_channels'] = cclist

    logging.debug("activationkey.checkConfigDeployment %s" % key)
    details['config_deploy'] = \
        self.client.activationkey.checkConfigDeployment(self.session, key)

    # Get group details, as the server group IDs are not necessarily the same
    # across servers, so we need the group name on import
    details['server_groups'] = []
    if details['server_group_ids']:
        grp_detail_list = []
        for grp in details['server_group_ids']:
            grp_details = self.client.systemgroup.getDetails(self.session, grp)

            if grp_details:
                grp_detail_list.append(grp_details)

        details['server_groups'] = [g['name'] for g in grp_detail_list]

    # Now append the details dict describing the key to the specified file
    return details


def do_activationkey_export(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    filename = ""
    if options.file != None:
        logging.debug("Passed filename do_activationkey_export %s" %
                      options.file)
        filename = options.file

    # Get the list of keys to export and sort out the filename if required
    keys = []
    if not args:
        if not filename:
            filename = "akey_all.json"
        logging.info("Exporting ALL activation keys to %s" % filename)
        keys = self.do_activationkey_list('', True)
    else:
        # allow globbing of activationkey names
        keys = filter_results(self.do_activationkey_list('', True), args)
        logging.debug("activationkey_export called with args %s, keys=%s" %
                      (args, keys))

        if not keys:
            logging.error("Invalid activation key passed")
            return

        if not filename:
            # No filename arg, so we try to do something sensible:
            # If we are exporting exactly one key, we default to keyname.json
            # otherwise, generic akeys.json name
            if len(keys) == 1:
                filename = "%s.json" % keys[0]
            else:
                filename = "akeys.json"

    # Dump as a list of dict
    keydetails_list = []
    for k in keys:
        logging.info("Exporting key %s to %s" % (k, filename))
        keydetails_list.append(self.export_activationkey_getdetails(k))

    logging.debug("About to dump %d keys to %s" %
                  (len(keydetails_list), filename))

    # Check if filepath exists, if it is an existing file
    # we prompt the user for confirmation
    if os.path.isfile(filename):
        if not self.user_confirm("File %s exists, confirm overwrite file? (y/n)" %
                                 filename):
            return

    if json_dump_to_file(keydetails_list, filename) != True:
        logging.error("Failed to save exported keys to file: {}".format(filename))
        return

####################


def help_activationkey_import(self):
    print('activationkey_import: import activation key(s) from JSON file(s)')
    print('''usage: activationkey_import <JSONFILE ...>''')


def do_activationkey_import(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        logging.error("No filename passed")
        self.help_activationkey_import()
        return

    for filename in args:
        logging.debug("Passed filename do_activationkey_import %s" % filename)
        keydetails_list = json_read_from_file(filename)

        if not keydetails_list:
            logging.error("Could not read json data from %s" % filename)
            return

        for keydetails in keydetails_list:
            if self.import_activationkey_fromdetails(keydetails) != True:
                logging.error("Failed to import key %s" %
                              keydetails['key'])

# create a new key based on the dict from export_activationkey_getdetails


def import_activationkey_fromdetails(self, keydetails):
    # First we check that an existing key with the same name does not exist
    existing_keys = self.do_activationkey_list('', True)

    if keydetails['key'] in existing_keys:
        logging.warning("%s already exists! Skipping!" % keydetails['key'])
        return False
    else:
        # create the key, we need to drop the org prefix from the key name
        keyname = re.sub('^[0-9]-', '', keydetails['key'])
        logging.debug("Found key %s, importing as %s" %
                      (keydetails['key'], keyname))

        # Channel label must be an empty-string for "Red Hat Satellite Default"
        # The export to json maps this to a unicode string "none"
        # To avoid changing the json format now, just fix it up here...
        if keydetails['base_channel_label'] == "none":
            keydetails['base_channel_label'] = ''

        if keydetails['usage_limit'] != 0:
            newkey = self.client.activationkey.create(self.session,
                                                      keyname,
                                                      keydetails['description'],
                                                      keydetails['base_channel_label'],
                                                      keydetails['usage_limit'],
                                                      keydetails['entitlements'],
                                                      keydetails['universal_default'])
        else:
            newkey = self.client.activationkey.create(self.session,
                                                      keyname,
                                                      keydetails['description'],
                                                      keydetails['base_channel_label'],
                                                      keydetails['entitlements'],
                                                      keydetails['universal_default'])
        if not newkey:
            logging.error("Failed to import key %s" %
                          keyname)
            return False

        # add child channels
        self.client.activationkey.addChildChannels(self.session, newkey,
                                                   keydetails['child_channel_labels'])

        # set config channel options and channels (missing are skipped)
        if keydetails['config_deploy'] != 0:
            self.client.activationkey.enableConfigDeployment(self.session,
                                                             newkey)
        else:
            self.client.activationkey.disableConfigDeployment(self.session,
                                                              newkey)

        if keydetails['config_channels']:
            self.client.activationkey.addConfigChannels(self.session, [newkey],
                                                        keydetails['config_channels'], False)

        # set groups (missing groups are created)
        gids = []
        for grp in keydetails['server_groups']:
            grpdetails = self.client.systemgroup.getDetails(self.session, grp)
            if grpdetails is None:
                logging.info("System group %s doesn't exist, creating" % grp)
                grpdetails = self.client.systemgroup.create(self.session, grp,
                                                            grp)
            gids.append(grpdetails.get('id'))

        if gids:
            logging.debug("Adding groups %s to key %s" % (gids, newkey))
            self.client.activationkey.addServerGroups(self.session, newkey,
                                                      gids)

        # Finally add the package list
        if keydetails['packages']:
            self.client.activationkey.addPackages(self.session, newkey,
                                                  keydetails['packages'])

        return True

####################


def help_activationkey_clone(self):
    print('activationkey_clone: Clone an activation key')
    print('''usage examples:)
                 activationkey_clone foo_key -c bar_key
                 activationkey_clone foo_key1 foo_key2 -c prefix
                 activationkey_clone foo_key -x "s/foo/bar"
                 activationkey_clone foo_key1 foo_key2 -x "s/foo/bar"

options:
  -c CLONE_NAME  : Name of the resulting key, treated as a prefix for multiple
                   keys
  -x "s/foo/bar" : Optional regex replacement, replaces foo with bar in the
                   clone description, base-channel label, child-channel
                   labels, config-channel names ''')


def complete_activationkey_clone(self, text, line, beg, end):
    return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_clone(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--clonename')
    arg_parser.add_argument('-x', '--regex')

    (args, options) = parse_command_arguments(args, arg_parser)
    allkeys = self.do_activationkey_list('', True)

    if is_interactive(options):
        print('')
        print('Activation Keys')
        print('------------------')
        print('\n'.join(sorted(allkeys)))
        print('')

        if len(args) == 1:
            print("Key to clone: %s" % args[0])
        else:
            # Clear out any args as interactive doesn't handle multiple keys
            args = []
            args.append(prompt_user('Original Key:', noblank=True))

        options.clonename = prompt_user('Cloned Key:', noblank=True)
    else:
        if not options.clonename and not options.regex:
            logging.error("Error - must specify either -c or -x options!")
            self.help_activationkey_clone()
            return

    if options.clonename in allkeys:
        logging.error("Key %s already exists" % options.clonename)
        return

    if not args:
        logging.error("Error no activationkey to clone passed!")
        self.help_activationkey_clone()
        return

    logging.debug("Got args=%s %d" % (args, len(args)))
    # allow globbing of configchannel channel names
    akeys = filter_results(allkeys, args)
    logging.debug("Filtered akeys %s" % akeys)
    logging.debug("all akeys %s" % allkeys)
    for ak in akeys:
        logging.debug("Cloning %s" % ak)
        # Replace the key-name with the clonename specified by the user
        keydetails = self.export_activationkey_getdetails(ak)

        # If the -x/--regex option is passed, do a sed-style replacement over
        # everything contained by the key.  This makes it easier to clone when
        # content is based on a known naming convention
        if options.regex:
            # formatted like a sed-replacement, s/foo/bar
            findstr = options.regex.split("/")[1]
            replacestr = options.regex.split("/")[2]
            logging.debug("Regex option with %s, replacing %s with %s" %
                          (options.regex, findstr, replacestr))

            # First we do the key name
            newkey = re.sub(findstr, replacestr, keydetails['key'])
            keydetails['key'] = newkey

            # Then the description
            newdesc = re.sub(findstr, replacestr, keydetails['description'])
            keydetails['description'] = newdesc

            # Then the base-channel label
            newbasech = re.sub(findstr, replacestr,
                               keydetails['base_channel_label'])
            if newbasech in self.list_base_channels():
                keydetails['base_channel_label'] = newbasech
                # Now iterate over any child-channel labels
                # we have the new base-channel, we can check if the new child
                # label exists under the new base-channel:
                # If it doesn't we can only skip it and print(a warning)
                all_childch = self.list_child_channels(system=None,
                                                       parent=newbasech, subscribed=False)

                new_child_channel_labels = []
                for c in keydetails['child_channel_labels']:
                    newc = re.sub(findstr, replacestr, c)
                    if newc in all_childch:
                        logging.debug("Found child channel %s for key %s, " %
                                      (c, keydetails['key']) +
                                      "replacing with %s" % newc)

                        new_child_channel_labels.append(newc)
                    else:
                        logging.warning("Found child channel %s key %s, %s" %
                                        (c, keydetails['key'], newc) +
                                        " does not exist, skipping!")

                logging.debug("Processed all child channels, " +
                              "new_child_channel_labels=%s" % new_child_channel_labels)

                keydetails['child_channel_labels'] = new_child_channel_labels
            else:
                logging.error("Regex-replacement results in new " +
                              "base-channel %s which does not exist!" % newbasech)

            # Finally, any config-channels
            new_config_channels = []
            allccs = self.do_configchannel_list('', True)
            for cc in keydetails['config_channels']:
                newcc = re.sub(findstr, replacestr, cc)

                if newcc in allccs:
                    logging.debug("Found config channel %s for key %s, " %
                                  (cc, keydetails['key']) +
                                  "replacing with %s" % newcc)

                    new_config_channels.append(newcc)
                else:
                    logging.warning("Found config channel %s for key %s, %s "
                                    % (cc, keydetails['key'], newcc) +
                                    "does not exist, skipping!")

            logging.debug("Processed all config channels, " +
                          "new_config_channels = %s" % new_config_channels)

            keydetails['config_channels'] = new_config_channels

        # Not regex mode
        elif options.clonename:
            if len(akeys) > 1:
                # We treat the clonename as a prefix for multiple keys
                # However we need to insert the prefix after the org-
                newkey = re.sub(r'^([0-9]-)', r'\1' + options.clonename,
                                keydetails['key'])
                keydetails['key'] = newkey
            else:
                keydetails['key'] = options.clonename

        logging.info("Cloning key %s as %s" % (ak, keydetails['key']))

        # Finally : import the key from the modified keydetails dict
        if self.import_activationkey_fromdetails(keydetails) != True:
            logging.error("Failed to clone %s to %s" %
                          (ak, keydetails['key']))

####################
# activationkey helper


def is_activationkey(self, name):
    if not name:
        return
    return name in self.do_activationkey_list(name, True)


def check_activationkey(self, name):
    if not name:
        logging.error("no activationkey label given")
        return False
    if not self.is_activationkey(name):
        logging.error("invalid activationkey label " + name)
        return False
    return True


def dump_activationkey(self, name, replacedict=None, excludes=None):
    content = self.do_activationkey_details(name)
    if not excludes:
        excludes = ["Universal Default:"]
    content = get_normalized_text(content, replacedict=replacedict, excludes=excludes)

    return content

####################


def help_activationkey_diff(self):
    print('activationkey_diff: Diff activation keys')
    print('')
    print('usage: activationkey_diff SOURCE_KEY TARGET_KEY')


def complete_activationkey_diff(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    if args == 3:
        return tab_completer(self.do_activationkey_list('', True), text)
    return []


def do_activationkey_diff(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) not in [1, 2]:
        self.help_activationkey_diff()
        return

    source_channel = args[0]
    if not self.check_activationkey(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_activationkey_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_activationkey_getcorresponding(source_channel)
    if not self.check_activationkey(target_channel):
        return

    source_replacedict, target_replacedict = get_string_diff_dicts(source_channel, target_channel)

    source_data = self.dump_activationkey(source_channel, source_replacedict)
    target_data = self.dump_activationkey(target_channel, target_replacedict)

    return diff(source_data, target_data, source_channel, target_channel)

####################


def help_activationkey_disable(self):
    print('activationkey_disable: Disable an activation key')
    print('')
    print('usage: activationkey_disable KEY [KEY ...]')


def complete_activationkey_disable(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) >= 2:
        return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_disable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 1:
        self.help_activationkey_disable()
        return

    keys = filter_results(self.do_activationkey_list('', True), args)

    details = {'disabled': True}

    for akey in keys:
        self.client.activationkey.setDetails(self.session, akey, details)

####################


def help_activationkey_enable(self):
    print('activationkey_enable: Enable an activation key')
    print('')
    print('usage: activationkey_enable KEY [KEY ...]')


def complete_activationkey_enable(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) >= 2:
        return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_enable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 1:
        self.help_activationkey_enable()
        return

    keys = filter_results(self.do_activationkey_list('', True), args)

    details = {'disabled': False}

    for akey in keys:
        self.client.activationkey.setDetails(self.session, akey, details)

####################


def help_activationkey_setdescription(self):
    print('activationkey_setdescription: Set the activation key description')
    print('')
    print('usage: activationkey_setdescription KEY DESCRIPTION')


def complete_activationkey_setdescription(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) <= 2:
        return tab_completer(self.do_activationkey_list('', True), text)


def do_activationkey_setdescription(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_activationkey_setdescription()
        return

    akey = args.pop(0)
    description = ' '.join(args)

    details = {'description': description}

    self.client.activationkey.setDetails(self.session, akey, details)

####################


def help_activationkey_setcontactmethod(self):
    print('activationkey_setcontactmethod: Set the contact method to use for ' \
          'systems registered with this key.')
    print('Available contact methods: ' + str(self.CONTACT_METHODS))
    print('usage: activationkey_setcontactmethod KEY CONTACT_METHOD')


def complete_activationkey_setcontactmethod(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_activationkey_list('', True), text)
    else:
        return tab_completer(self.CONTACT_METHODS, text)


def do_activationkey_setcontactmethod(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) == 2:
        self.help_activationkey_setcontactmethod()
        return

    details = {'contact_method': args.pop()}
    akey = args.pop()

    self.client.activationkey.setDetails(self.session, akey, details)
07070100000015000081B40000000000000000000000015DA8415F00000B67000000000000000000000000000000000000001D00000000spacecmd/src/spacecmd/api.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2011 Satoru SATOH <ssato@redhat.com>
#

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

import codecs
import logging
import sys
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_api(self):
    print('api: call RHN API with arguements directly')
    print('''usage: api [options] API_STRING)

options:
  -A, --args  Arguments for the API other than session id in comma separated
              strings or JSON expression
  -F, --format   Output format
  -o, --output   Output file

examples:
  api api.getApiCallList
  api --args "sysgroup_A" systemgroup.listSystems
  api -A "rhel-i386-server-5,2011-04-01,2011-05-01" -F "%(name)s" \\
      channel.software.listAllPackages
''')


def do_api(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-A', '--args', default='')
    arg_parser.add_argument('-F', '--format', default='')
    arg_parser.add_argument('-o', '--output', default='')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_api()
        return

    api_name = args[0]
    api_args = parse_api_args(options.args)

    if options.output:
        try:
            output = open(options.output, "w")
        except IOError:
            logging.warning("Could not open to write: " + options.output)
            logging.info("Fallback output to stdout")

            output = sys.stdout
    else:
        output = sys.stdout

    api = getattr(self.client, api_name, None)

    if not callable(api):
        logging.warning("No such API: " + api_name)
        return

    try:
        res = api(self.session, *api_args)

        if not isinstance(res, list):
            res = [res]

        if options.format:
            for r in res:
                output.write(options.format % r + "\n")
        else:
            json_dump(res, output, indent=2, cls=CustomJsonEncoder)

        if (output != sys.stdout):
            output.close()

    except xmlrpclib.Fault:
        if (output != sys.stdout):
            output.close()
07070100000016000081B40000000000000000000000015DA8415F0000054C000000000000000000000000000000000000002800000000spacecmd/src/spacecmd/argumentparser.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from argparse import ArgumentParser

# argparse by default will exit when there is an error.  when spacecmd
# is in an interactive shell, we don't want to exit.  instead, just
# raise an exception that will printed for the user to read.


class SpacecmdArgumentParser(ArgumentParser):

    def error(self, message):
        raise Exception(message)
07070100000017000081B40000000000000000000000015DA8415F0000E81A000000000000000000000000000000000000002700000000spacecmd/src/spacecmd/configchannel.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from datetime import datetime
import base64
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *

def help_configchannel_list(self):
    print('configchannel_list: List all configuration channels')
    print('usage: configchannel_list')


def do_configchannel_list(self, args, doreturn=False):
    channels = self.client.configchannel.listGlobals(self.session)
    channels = [c.get('label') for c in channels]

    if doreturn:
        return channels
    else:
        if channels:
            print('\n'.join(sorted(channels)))

####################


def help_configchannel_listsystems(self):
    print('configchannel_listsystems: List the systems subscribed to a')
    print('                           configuration channel')
    print('usage: configchannel_listsystems CHANNEL')


def complete_configchannel_listsystems(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_listsystems(self, args):
    if not self.check_api_version('10.11'):
        logging.warning("This version of the API doesn't support this method")
        return

    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_configchannel_listsystems()
        return

    channel = args[0]

    systems = self.client.configchannel.listSubscribedSystems(self.session,
                                                              channel)

    systems = sorted([s.get('name') for s in systems])

    if systems:
        print('\n'.join(systems))

####################


def help_configchannel_listfiles(self):
    print('configchannel_listfiles: List the files in a config channel')
    print('usage: configchannel_listfiles CHANNEL ...')


def complete_configchannel_listfiles(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_listfiles(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_configchannel_listfiles()
        return []

    for channel in args:
        files = self.client.configchannel.listFiles(self.session,
                                                    channel)
        files = [f.get('path') for f in files]

        if doreturn:
            return files
        else:
            if files:
                print('\n'.join(sorted(files)))

####################


def help_configchannel_forcedeploy(self):
    print('configchannel_forcedeploy: Forces a redeployment')
    print('                           of files within this channel')
    print('                           on all subscribed systems')
    print('usage: configchannel_forcedeploy CHANNEL')


def complete_configchannel_forcedeploy(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_forcedeploy(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_configchannel_forcedeploy()
        return

    channel = args[0]

    files = self.client.configchannel.listFiles(self.session, channel)
    files = [f.get('path') for f in files]

    if not files:
        print('No files within selected configchannel.')
        return
    else:
        systems = self.client.configchannel.listSubscribedSystems(self.session, channel)
        systems = sorted([s.get('name') for s in systems])
        if not systems:
            print('Channel has no subscribed Systems')
            return
        else:
            print('Force deployment of the following configfiles:')
            print('==============================================')
            print('\n'.join(files))
            print('\nOn these systems:')
            print('=================')
            print('\n'.join(systems))
    if self.user_confirm('Really force deployment [y/N]:'):
        self.client.configchannel.deployAllSystems(self.session, channel)

####################


def help_configchannel_filedetails(self):
    print('configchannel_filedetails: Show the details of a file')
    print('in a configuration channel')
    print('usage: configchannel_filedetails CHANNEL FILE [REVISION]')


def complete_configchannel_filedetails(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_configchannel_list('', True),
                             text)
    if len(parts) > 2:
        return tab_completer(
            self.do_configchannel_listfiles(parts[1], True), text)

    return []


def do_configchannel_filedetails(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_configchannel_filedetails()
        return

    channel = args[0]
    filename = args[1]
    revision = None

    try:
        revision = int(args[2])
    except (ValueError, IndexError):
        pass

    # the server return a null exception if an invalid file is passed
    valid_files = self.do_configchannel_listfiles(channel, True)
    if not filename in valid_files:
        logging.warning('%s is not in this configuration channel' % filename)
        return

    if revision:
        details = self.client.configchannel.lookupFileInfo(self.session,
                                                           channel,
                                                           filename,
                                                           revision)
    else:
        results = self.client.configchannel.lookupFileInfo(self.session,
                                                           channel,
                                                           [filename])

        # grab the first item since we only do one file
        details = results[0]

    result = []
    result.append('Path:     %s' % details.get('path'))
    result.append('Type:     %s' % details.get('type'))
    result.append('Revision: %i' % details.get('revision'))
    result.append('Created:  %s' % details.get('creation'))
    result.append('Modified: %s' % details.get('modified'))

    if details.get('type') == 'symlink':
        result.append('')
        result.append('Target Path:     %s' % details.get('target_path'))
    else:
        result.append('')
        result.append('Owner:           %s' % details.get('owner'))
        result.append('Group:           %s' % details.get('group'))
        result.append('Mode:            %s' % details.get('permissions_mode'))

    result.append('SELinux Context: %s' % details.get('selinux_ctx'))

    if details.get('type') == 'file':
        result.append('SHA256:          %s' % details.get('sha256'))
        result.append('Binary:          %s' % details.get('binary'))

        if not details.get('binary'):
            result.append('')
            result.append('Contents')
            result.append('--------')
            result.append(details.get('contents'))

    return result

####################


def help_configchannel_backup(self):
    print('configchannel_backup: backup a config channel')
    print('''usage: configchannel_backup CHANNEL [OUTDIR])

OUTDIR defaults to $HOME/spacecmd-backup/configchannel/YYYY-MM-DD/CHANNEL
''')


def complete_configchannel_backup(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_backup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 1:
        self.help_configchannel_backup()
        return

    channel = args[0]

    # use an output base from the user if it was passed
    if len(args) == 2:
        outputpath_base = datetime.now().strftime(os.path.expanduser(args[1]))
    else:
        outputpath_base = os.path.expanduser('~/spacecmd-backup/configchannel')

        # make the final output path be <base>/date/channel
        outputpath_base = os.path.join(outputpath_base,
                                       datetime.now().strftime("%Y-%m-%d"),
                                       channel)

    try:
        if not os.path.isdir(outputpath_base):
            os.makedirs(outputpath_base)
    except OSError:
        logging.error('Could not create output directory')
        return

    # the server return a null exception if an invalid file is passed
    valid_files = self.do_configchannel_listfiles(channel, True)
    results = self.client.configchannel.lookupFileInfo(self.session,
                                                       channel,
                                                       valid_files)

    try:
        fh = open(outputpath_base + "/.metainfo", 'w')
    except IOError:
        logging.error('Could not create metainfo file')
        return

    for details in results:
        dumpfile = outputpath_base + details.get('path')
        dumpdir = dumpfile
        print('Output Path:   %s' % dumpfile)
        fh.write('[%s]\n' % details.get('path'))
        fh.write('type = %s\n' % details.get('type'))
        fh.write('revision = %s\n' % details.get('revision'))
        fh.write('creation = %s\n' % details.get('creation'))
        fh.write('modified = %s\n' % details.get('modified'))

        if details.get('type') == 'symlink':
            fh.write('target_path = %s\n' % details.get('target_path'))
        else:
            fh.write('owner = %s\n' % details.get('owner'))
            fh.write('group = %s\n' % details.get('group'))
            fh.write('permissions_mode = %s\n' % details.get('permissions_mode'))

        fh.write('selinux_ctx = %s\n' % details.get('selinux_ctx'))

        if details.get('type') == 'file':
            dumpdir = os.path.dirname(dumpfile)

        if not os.path.isdir(dumpdir):
            os.makedirs(dumpdir)

        if details.get('type') == 'file':
            fh.write('sha256 = %s\n' % details.get('sha256'))
            fh.write('binary = %s\n' % details.get('binary'))
            of = open(dumpfile, 'w')
            of.write(details.get('contents') or '')
            of.close()

        fh.write('\n')

    fh.close()

####################


def help_configchannel_details(self):
    print('configchannel_details: Show the details of a config channel')
    print('usage: configchannel_details CHANNEL ...')


def complete_configchannel_details(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_configchannel_details()
        return

    add_separator = False

    result = []
    for channel in args:
        details = self.client.configchannel.getDetails(self.session,
                                                       channel)

        files = self.client.configchannel.listFiles(self.session,
                                                    channel)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        result.append('Label:       %s' % details.get('label'))
        result.append('Name:        %s' % details.get('name'))
        result.append('Description: %s' % details.get('description'))
        result.append('Type:        %s' % details.get('configChannelType').get('label'))

        result.append('')
        result.append('Files')
        result.append('-----')
        for f in files:
            result.append(f.get('path'))
    return result

####################


def help_configchannel_create(self):
    print('configchannel_create: Create a configuration channel of specific type')
    print('''usage: configchannel_create [options])

options:
  -n NAME
  -l LABEL
  -d DESCRIPTION 
  -t TYPE('normal,'state')''')


def do_configchannel_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-l', '--label')
    arg_parser.add_argument('-d', '--description')
    arg_parser.add_argument('-t', '--type')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name:', noblank=True)
        options.label = prompt_user('Label:')
        options.description = prompt_user('Description:')
        options.type = prompt_user('Type[normal, state]:')

        if options.label == '':
            options.label = options.name
        if options.description == '':
            options.description = options.name
        if options.type not in ('normal', 'state'):
            logging.error('Only [normal/state] values are acceptable for --type')
            return
    else:
        if not options.name:
            logging.error('A name is required')
            return
        if not options.label:
            options.label = options.name
        if not options.description:
            options.description = options.name
        if not options.type:
            options.type = 'normal'
        if options.type not in ('normal', 'state'):
            logging.error('Only [normal/state] values are acceptable for --type')
            return

    self.client.configchannel.create(self.session,
                                     options.label,
                                     options.name,
                                     options.description,
                                     options.type)

####################


def help_configchannel_delete(self):
    print('configchannel_delete: Delete a configuration channel')
    print('usage: configchannel_delete CHANNEL ...')


def complete_configchannel_delete(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_configchannel_delete()
        return

    # allow globbing of configchannel names
    channels = filter_results(self.do_configchannel_list('', True), args)
    logging.debug("configchannel_delete called with args %s, channels=%s" %
                  (args, channels))

    if not channels:
        logging.error("No channels matched argument %s" % args)
        return

    # Print the channels prior to the confirmation
    print('\n'.join(sorted(channels)))

    if self.user_confirm('Delete these channels [y/N]:'):
        self.client.configchannel.deleteChannels(self.session, channels)

####################


def configfile_getinfo(self, args, options, file_info=None, interactive=False):
    # Common code which is used in both configchannel_addfile and
    # system_addconfigfile.  Takes args/options from each call and
    # returns the file_info dict needed to create the file in either
    # the configchannel or sytem sandbox/local-override respectively
    #
    # file_info is the existing info from lookupFileInfo or None if
    # no file for this path exists already

    # initialize here instead of multiple times below
    contents = ''

    if interactive:
        # use existing values if available
        if file_info:
            for info in file_info:
                if info.get('path') == options.path:
                    logging.debug('Found existing file in channel')

                    options.owner = info.get('owner')
                    options.group = info.get('group')
                    options.mode = info.get('permissions_mode')
                    options.target_path = info.get('target_path')
                    options.selinux_ctx = info.get('selinux_ctx')
                    contents = info.get('contents')

                    if info.get('type') == 'symlink':
                        options.symlink = True

        if not options.owner:
            options.owner = 'root'
        if not options.group:
            options.group = 'root'

        # if this is a new file, ask if it's a symlink
        if not options.symlink:
            userinput = prompt_user('Symlink [y/N]:')
            options.symlink = re.match('y', userinput, re.I)

        if options.symlink:
            target_input = prompt_user('Target Path:', noblank=True)
            selinux_input = prompt_user('SELinux Context [none]:')

            if target_input:
                options.target_path = target_input

            if selinux_input:
                options.selinux_ctx = selinux_input
        else:
            userinput = prompt_user('Directory [y/N]:')
            options.directory = re.match('y', userinput, re.I)

            if not options.mode:
                if options.directory:
                    options.mode = '0755'
                else:
                    options.mode = '0644'

            owner_input = prompt_user('Owner [%s]:' % options.owner)
            group_input = prompt_user('Group [%s]:' % options.group)
            mode_input = prompt_user('Mode [%s]:' % options.mode)
            selinux_input = \
                prompt_user('SELinux Context [%s]:' % options.selinux_ctx)
            revision_input = prompt_user('Revision [next]:')

            if owner_input:
                options.owner = owner_input

            if group_input:
                options.group = group_input

            if mode_input:
                options.mode = mode_input

            if selinux_input:
                options.selinux_ctx = selinux_input

            if revision_input:
                try:
                    options.revision = int(revision_input)
                except ValueError:
                    logging.warning('The revision must be an integer')

            if not options.directory:
                if self.user_confirm('Read an existing file [y/N]:',
                                     nospacer=True, ignore_yes=True):
                    options.file = prompt_user('File:')

                    contents = read_file(options.file)

                    if options.binary is None and self.file_is_binary(options.file):
                        options.binary = True
                        logging.debug("Binary detected")
                    elif options.binary:
                        logging.debug("Binary selected")
                else:
                    if contents:
                        template = contents
                    else:
                        template = ''

                    (contents, _ignore) = editor(template=template, delete=True)
    else:
        if not options.path:
            logging.error('The path is required')
            return

        if not options.symlink and not options.directory:
            if options.file:
                contents = read_file(options.file)

                if options.binary is None:
                    options.binary = self.file_is_binary(options.file)
                    if options.binary:
                        logging.debug("Binary detected")
                elif options.binary:
                    logging.debug("Binary selected")
            else:
                logging.error('You must provide the file contents')
                return

        if options.symlink and not options.target_path:
            logging.error('You must provide the target path for a symlink')
            return

    # selinux_ctx can't be None
    if not options.selinux_ctx:
        options.selinux_ctx = ''

    # directory can't be None
    if not options.directory:
        options.directory = False

    if options.symlink:
        file_info = {'target_path': options.target_path,
                     'selinux_ctx': options.selinux_ctx}

        print('Path:            %s' % options.path)
        print('Target Path:     %s' % file_info['target_path'])
        print('SELinux Context: %s' % file_info['selinux_ctx'])
    else:
        if not options.owner:
            options.owner = 'root'
        if not options.group:
            options.group = 'root'
        if not options.mode:
            if options.directory:
                options.mode = '0755'
            else:
                options.mode = '0644'

        logging.debug("base64 encoding contents")
        contents = base64.b64encode(contents.encode('utf8')).decode()

        file_info = {'contents': ''.join(contents),
                     'owner': options.owner,
                     'group': options.group,
                     'selinux_ctx': options.selinux_ctx,
                     'permissions': options.mode,
                     'contents_enc64': True,
                     'binary': options.binary}

        # Binary set or detected
        if options.binary:
            file_info['binary'] = True

        print('Path:            %s' % options.path)
        print('Directory:       %s' % options.directory)
        print('Owner:           %s' % file_info['owner'])
        print('Group:           %s' % file_info['group'])
        print('Mode:            %s' % file_info['permissions'])
        print('Binary:          %s' % file_info['binary'])
        print('SELinux Context: %s' % file_info['selinux_ctx'])

        # only add the revision field if the user supplied it
        if options.revision:
            file_info['revision'] = int(options.revision)
            print('Revision:        %i' % file_info['revision'])

        if not options.directory:
            print('')
            if options.binary:
                print('Contents not displayed (base64 encoded)')
            else:
                print('Contents')
                print('--------')
                if file_info['contents_enc64']:
                    print(base64.b64decode(file_info['contents']))
                else:
                    print(file_info['contents'])

    return file_info


def help_configchannel_addfile(self):
    print('configchannel_addfile/configchannel_updatefile: Create a configuration file')
    print('''usage: configchannel_addfile/configchannel_updatefile -c CHANNEL - p PATH -f LOCAL_FILE_PATH [OPTIONS])

options:
  -c CHANNEL
  -p PATH
  -r REVISION
  -o OWNER [default: root]
  -g GROUP [default: root]
  -m MODE [defualt: 0644]
  -x SELINUX_CONTEXT
  -d path is a directory
  -s path is a symlink
  -b path is a binary (or other file which needs base64 encoding)
  -t SYMLINK_TARGET
  -f local path to file contents
  -y automatically proceed with file contents

  Note re binary/base64: Some text files, notably those containing trailing
  newlines, those containing ASCII escape characters (or other charaters not
  allowed in XML) need to be sent as binary (-b).  Some effort is made to auto-
  detect files which require this, but you may need to explicitly specify.
''')


def complete_configchannel_addfile(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_addfile(self, args, update_path=''):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--channel')
    arg_parser.add_argument('-p', '--path')
    arg_parser.add_argument('-o', '--owner')
    arg_parser.add_argument('-g', '--group')
    arg_parser.add_argument('-m', '--mode')
    arg_parser.add_argument('-x', '--selinux-ctx')
    arg_parser.add_argument('-t', '--target-path')
    arg_parser.add_argument('-f', '--file')
    arg_parser.add_argument('-r', '--revision')
    arg_parser.add_argument('-s', '--symlink', action='store_true')
    arg_parser.add_argument('-b', '--binary', action='store_true')
    arg_parser.add_argument('-d', '--directory', action='store_true')
    arg_parser.add_argument('-y', '--yes', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    file_info = None

    interactive = is_interactive(options)
    if interactive:
        # the channel name can be passed in
        if args:
            options.channel = args[0]
        else:
            while True:
                print('Configuration Channels')
                print('----------------------')
                print('\n'.join(sorted(self.do_configchannel_list('', True))))
                print('')

                options.channel = prompt_user('Select:', noblank=True)

                # ensure the user enters a valid configuration channel
                if options.channel in self.do_configchannel_list('', True):
                    break
                else:
                    print('')
                    logging.warning('%s is not a valid channel' %
                                    options.channel)
                    print('')

        if update_path:
            options.path = update_path
        else:
            options.path = prompt_user('Path:', noblank=True)

        # check if this file already exists
        try:
            file_info = \
                self.client.configchannel.lookupFileInfo(self.session,
                                                         options.channel,
                                                         [options.path])
        except xmlrpclib.Fault:
            logging.debug("No existing file information found for %s" %
                          options.path)
            file_info = None

    file_info = self.configfile_getinfo(args, options, file_info, interactive)

    if not options.channel:
        logging.error("No config channel specified!")
        self.help_configchannel_addfile()
        return

    if not file_info:
        logging.error("Error obtaining file info")
        self.help_configchannel_addfile()
        return

    if options.yes or self.user_confirm():
        if options.symlink:
            self.client.configchannel.createOrUpdateSymlink(self.session,
                                                            options.channel,
                                                            options.path,
                                                            file_info)
        else:
            # compatibility for Satellite 5.3
            if not self.check_api_version('10.11'):
                del file_info['selinux_ctx']

                if 'revision' in file_info:
                    del file_info['revision']

            if options.directory:
                if 'contents' in file_info:
                    del file_info['contents']

            self.client.configchannel.createOrUpdatePath(self.session,
                                                         options.channel,
                                                         options.path,
                                                         options.directory,
                                                         file_info)

####################

def help_configchannel_updateinitsls(self):
    print('configchannel_updateinitsls: Update init.sls file')
    print('''usage: configchannel_updateinitsls -c CHANNEL -f LOCAL_FILE_PATH [OPTIONS])

options:
  -c CHANNEL
  -f local path to file contents
  -y automatically proceed with file contents
''')

def do_configchannel_updateinitsls(self, args, update_path=''):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--channel')
    arg_parser.add_argument('-f', '--file')
    arg_parser.add_argument('-y', '--yes', action='store_true')
    (args, options) = parse_command_arguments(args, arg_parser)

    file_info = None
    path = "/init.sls"
    interactive = is_interactive(options)
    if interactive:
        # the channel name can be passed in
        if args:
            options.channel = args[0]
        else:
            while True:
                print('Configuration Channels')
                print('----------------------')
                print('\n'.join(sorted(self.do_configchannel_list('', True))))
                print('')

                options.channel = prompt_user('Select:', noblank=True)

                # ensure the user enters a valid configuration channel
                if options.channel in self.do_configchannel_list('', True):
                    break
                else:
                    print('')
                    logging.warning('%s is not a valid channel' %
                                    options.channel)
                    print('')
        # check if this file already exists
        try:
            file_info = \
               self.client.configchannel.lookupFileInfo(self.session,
                                                         options.channel,
                                                         [path])
        except xmlrpclib.Fault:
            logging.error("No existing file information found for %s" %
                          options.path)
            return
        contents = file_info[0].get('contents')
        if self.user_confirm('Read an existing file [y/N]:',
                         nospacer=True, ignore_yes=True):
            options.file = prompt_user('File:')
            contents = read_file(options.file)
            if self.file_is_binary(options.file):
                logging.debug("Binary selected")
        else:
            if contents:
                template = contents
            else:
                template = ''
            (contents, _ignore) = editor(template=template, delete=True)

    else:
        if options.file:
            contents = read_file(options.file)
        else:
            logging.error('You must provide the file contents')
            return

    contents = base64.b64encode(contents.encode('utf8')).decode()

    file_info = {'contents': ''.join(contents),
                 'contents_enc64': True
                }


    if not options.channel:
        logging.error("No config channel specified!")
        self.help_configchannel_updateinitsls()
        return

    if not file_info:
        logging.error("Error obtaining file info")
        self.help_configchannel_updateinitsls()
        return
    print('contents_enc64:           %s' % file_info['contents_enc64'])
    print('Contents')
    print('--------')
    print(base64.b64decode(file_info['contents']))
    if options.yes or self.user_confirm():
        self.client.configchannel.updateInitSls(self.session, options.channel, file_info)

####################


def help_configchannel_updatefile(self):
    self.help_configchannel_addfile()


def complete_configchannel_updatefile(self, text, line, beg, end):
    return self.complete_configchannel_addfile(text, line, beg, end)


def do_configchannel_updatefile(self, args):
    return self.do_configchannel_addfile(args)

####################


def help_configchannel_removefiles(self):
    print('configchannel_removefiles: Remove configuration files')
    print('usage: configchannel_removefiles CHANNEL <FILE ...>')


def complete_configchannel_removefiles(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_configchannel_list('', True),
                             text)
    elif len(parts) > 2:
        channel = parts[1]
        return tab_completer(self.do_configchannel_listfiles(channel,
                                                             True),
                             text)


def do_configchannel_removefiles(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_configchannel_removefiles()
        return

    channel = args.pop(0)
    files = args

    if self.user_confirm('Remove these files [y/N]:'):
        self.client.configchannel.deleteFiles(self.session, channel, files)

####################


def help_configchannel_verifyfile(self):
    print('configchannel_verifyfile: Verify a configuration file')
    print('usage: configchannel_verifyfile CHANNEL FILE <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_configchannel_verifyfile(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_configchannel_list('', True), text)
    elif len(parts) == 3:
        channel = parts[1]
        return tab_completer(self.do_configchannel_listfiles(channel, True),
                             text)
    elif len(parts) > 3:
        return self.tab_complete_systems(text)


def do_configchannel_verifyfile(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_configchannel_verifyfile()
        return

    channel = args[0]
    path = args[1]

    # use the systems listed in the SSM
    if re.match('ssm', args[2], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args[2:])

    system_ids = [self.get_system_id(s) for s in systems]

    if not system_ids:
        logging.error('No valid system selected')
        return
    action_id = \
        self.client.configchannel.scheduleFileComparisons(self.session,
                                                          channel,
                                                          path,
                                                          system_ids)

    logging.info('Action ID: %i' % action_id)

####################


def help_configchannel_export(self):
    print('configchannel_export: export config channel(s) to json format file')
    print('''usage: configchannel_export <CHANNEL>... [options])
options:
    -f outfile.json : specify an output filename, defaults to <CHANNEL>.json
                      if exporting a single channel, ccs.json for multiple
                      channels, or cc_all.json if no CHANNEL specified
                      e.g (export ALL)

Note : CHANNEL list is optional, default is to export ALL''')


def complete_configchannel_export(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def export_configchannel_getdetails(self, channel):
    # Get the cc details
    logging.info("Getting config channel details for %s" % channel)
    details = self.client.configchannel.getDetails(self.session, channel)
    files = self.client.configchannel.listFiles(self.session, channel)
    details['files'] = []
    paths = [f['path'] for f in files]
    fileinfo = []
    # Some versions of the API blow up when lookupFileInfo is asked to
    # return details of files containing non-XML-valid characters.
    # later API versions simply return empty file contents, but to
    # ensure the least-bad operation with older (sat 5.3) API versions
    # we can iterate over each file, then we just error on individual files
    # instead of failing to export anything at all...
    for p in paths:
        logging.debug("Found file %s for %s" % (p, channel))
        try:
            pinfo = self.client.configchannel.lookupFileInfo(self.session,
                                                             channel, [p])
            if pinfo:
                fileinfo.append(pinfo[0])
        except xmlrpclib.Fault:
            logging.error("Failed to get details for file %s from %s"
                          % (p, channel))
    # Now we strip the datetime fields from the Info structs, as they
    # are not JSON serializable with the default encoder, and we don't
    # need them on import anyway
    # We also strip some other fields which are not useful on import
    # This is a bit complicated because the createOrUpdateFoo functions
    # take two different struct formats, which are both different to
    # the format returned by lookupFileInfo, doh!
    # We get:                         We need:
    #                                 (file/dir)      (symlink)
    # string "type"                   Y               Y
    # string "path"                   Y               Y
    # string "target_path"            N               Y
    # string "channel"                N               N
    # string "contents"               Y               N
    # int "revision"                  N (auto)        N (auto)
    # dateTime.iso8601 "creation"     N               N
    # dateTime.iso8601 "modified"     N               N
    # string "owner"                  Y               N
    # string "group"                  Y               N
    # int "permissions"               Y (as string!)  N
    # string "permissions_mode"       N               N
    # string "selinux_ctx"            Y               Y
    # boolean "binary"                Y               N
    # string "sha256"                 N               N
    # string "macro-start-delimiter"  Y               N
    # string "macro-end-delimiter"    Y               N
    for f in fileinfo:

        if f['type'] == 'symlink':
            for k in ['contents', 'owner', 'group', 'permissions',
                      'macro-start-delimiter', 'macro-end-delimiter']:
                if k in f:
                    del f[k]
        else:
            if 'target_path' in f:
                del f['target_path']
            f['permissions'] = str(f['permissions'])

            # If we're using a recent API version files exported with no contents
            # i.e binary or non-xml encodable ascii files can be exported as
            # base64 encoded
            if not 'contents' in f:
                if f['type'] != 'directory':
                    if not self.check_api_version('11.1'):
                        logging.warning("File %s could not be exported " % f['path'] +
                                        "with this API version(needs base64 encoding)")
                    else:
                        logging.info("File %s could not be exported as" % f['path'] +
                                     " text...getting base64 encoded version")
                        b64f = self.client.configchannel.getEncodedFileRevision(
                            self.session, channel, f['path'], f['revision'])
                        f['contents'] = b64f['contents']
                        f['contents_enc64'] = b64f['contents_enc64']

        for k in ['channel', 'revision', 'creation', 'modified',
                  'permissions_mode', 'sha256']:
            if k in f:
                del f[k]

    details['files'] = fileinfo
    return details


def do_configchannel_export(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    filename = ""
    if options.file != None:
        logging.debug("Passed filename do_configchannel_export %s" %
                      options.file)
        filename = options.file

    # Get the list of ccs to export and sort out the filename if required
    ccs = []
    if not args:
        if not filename:
            filename = "cc_all.json"
        logging.info("Exporting ALL config channels to %s" % filename)
        ccs = self.do_configchannel_list('', True)
    else:
        # allow globbing of configchannel names
        ccs = filter_results(self.do_configchannel_list('', True), args)
        logging.debug("configchannel_export called with args %s, ccs=%s" %
                      (args, ccs))
        if not ccs:
            logging.error("Error, no valid config channel passed, " +
                          "check name is  correct with spacecmd configchannel_list")
            return
        if not filename:
            # No filename arg, so we try to do something sensible:
            # If we are exporting exactly one cc, we default to ccname.json
            # otherwise, generic ccs.json name
            if len(ccs) == 1:
                filename = "%s.json" % ccs[0]
            else:
                filename = "ccs.json"

    # Dump as a list of dict
    ccdetails_list = []
    for c in ccs:
        logging.info("Exporting cc %s to %s" % (c, filename))
        ccdetails_list.append(self.export_configchannel_getdetails(c))

    logging.debug("About to dump %d ccs to %s" %
                  (len(ccdetails_list), filename))
    # Check if filepath exists, if it is an existing file
    # we prompt the user for confirmation
    if os.path.isfile(filename):
        if not self.user_confirm("File %s exists, " % filename +
                                 "confirm overwrite file? (y/n)"):
            return
    if json_dump_to_file(ccdetails_list, filename) != True:
        logging.error("Error saving exported config channels to file" %
                      filename)
        return

####################


def help_configchannel_import(self):
    print('configchannel_import: import config channel(s) from json file')
    print('''usage: configchannel_import <JSONFILES...>''')


def do_configchannel_import(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        logging.error("Error, no filename passed")
        self.help_configchannel_import()
        return

    for filename in args:
        logging.debug("Passed filename do_configchannel_import %s" % filename)
        ccdetails_list = json_read_from_file(filename)
        if not ccdetails_list:
            logging.error("Error, could not read json data from %s" % filename)
            return
        for ccdetails in ccdetails_list:
            if self.import_configchannel_fromdetails(ccdetails) != True:
                logging.error("Error importing configchannel %s" %
                              ccdetails['name'])

# create a new cc based on the dict from export_configchannel_getdetails


def import_configchannel_fromdetails(self, ccdetails):

    # First we check that an existing channel with the same name does not exist
    existing_ccs = self.do_configchannel_list('', True)
    if ccdetails['name'] in existing_ccs:
        logging.warning("Config channel %s already exists! Skipping!" %
                        ccdetails['name'])
        return False
    else:
        # create the cc, we need to drop the org prefix from the cc name
        logging.info("Importing config channel  %s" % ccdetails['name'])

        channeltype = 'normal'
        if 'configChannelType' in ccdetails:
            channeltype = ccdetails['configChannelType']['label']

        # Create the channel
        self.client.configchannel.create(self.session,
                                         ccdetails['label'],
                                         ccdetails['name'],
                                         ccdetails['description'],
                                         channeltype)
        for filedetails in ccdetails['files']:
            path = filedetails['path']
            del filedetails['path']
            logging.info("Found %s %s for cc %s" %
                         (filedetails['type'], path, ccdetails['name']))
            ret = None
            if filedetails['type'] == 'symlink':
                del filedetails['type']
                logging.debug("Adding symlink %s" % filedetails)
                ret = self.client.configchannel.createOrUpdateSymlink(
                    self.session, ccdetails['label'], path, filedetails)
            elif filedetails['type'] == 'sls':
                # Filter out everything except the file contents:
                init_sls_details = {k:v for (k,v) in filedetails.items() if k in ['contents', 'contents_enc64']}
                ret = self.client.configchannel.updateInitSls(
                    self.session, ccdetails['label'], init_sls_details)
            else:
                if filedetails['type'] == 'directory':
                    isdir = True
                    if 'contents' in filedetails:
                        del filedetails['contents']
                else:
                    isdir = False
                    # If binary files (or those containing characters which are
                    # invalid in XML, e.g the ascii escape character) are
                    # exported, on older API versions, you end up with a file
                    # with no "contents" key (
                    # I guess the best thing to do here flag an error and
                    # import everything else
                    if not 'contents' in filedetails:
                        logging.error(
                            "Failed trying to import file %s (empty content)"
                            % path)
                        logging.error("Older APIs can't export encoded files")
                        continue

                    if not filedetails['contents_enc64']:
                        logging.debug("base64 encoding file")
                        filedetails['contents'] = \
                            base64.b64encode(filedetails['contents'].encode('utf8'))

                        #change bytes to string before sending
                        filedetails['contents'] =  filedetails['contents'].decode('utf8')
                        filedetails['contents_enc64'] = True

                logging.debug("Creating %s %s" %
                              (filedetails['type'], filedetails))
                if 'type' in filedetails:
                    del filedetails['type']

                ret = self.client.configchannel.createOrUpdatePath(
                    self.session, ccdetails['label'], path, isdir, filedetails)
            if ret != None:
                logging.debug("Added file %s to %s" %
                              (ret['path'], ccdetails['name']))
            else:
                logging.error("Error adding file %s to %s" %
                              (filedetails['path'], ccdetails['label']))
                continue

    return True

####################


def help_configchannel_clone(self):
    print('configchannel_clone: Clone config channel(s)')
    print('''usage examples:)
                 configchannel_clone foo_label -c bar_label
                 configchannel_clone foo_label1 foo_label2 -c prefix
                 configchannel_clone foo_label -x "s/foo/bar"
                 configchannel_clone foo_label1 foo_label2 -x "s/foo/bar"

options:
  -c CLONE_LABEL : name/label of the resulting cc (note does not update
                   description, see -x option), treated as a prefix if
                   multiple keys are passed
  -x "s/foo/bar" : Optional regex replacement, replaces foo with bar in the
                   clone name, label and description
  Note : If no -c or -x option is specified, interactive is assumed''')


def complete_configchannel_clone(self, text, line, beg, end):
    return tab_completer(self.do_configchannel_list('', True), text)


def do_configchannel_clone(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--clonelabel')
    arg_parser.add_argument('-x', '--regex')

    (args, options) = parse_command_arguments(args, arg_parser)
    allccs = self.do_configchannel_list('', True)

    if is_interactive(options):
        print('')
        print('Config Channels')
        print('------------------')
        print('\n'.join(sorted(allccs)))
        print('')

        if len(args) == 1:
            print("Channel to clone: %s" % args[0])
        else:
            # Clear out any args as interactive doesn't handle multiple ccs
            args = []
            args.append(prompt_user('Channel to clone:', noblank=True))
        options.clonelabel = prompt_user('Clone label:', noblank=True)
    else:
        if not options.clonelabel and not options.regex:
            logging.error("Error - must specify either -c or -x options!")
            self.help_configchannel_clone()
        else:
            logging.debug("%s : %s" % (options.clonelabel, options.regex))

    if not args:
        logging.error("Error no channel label passed!")
        self.help_configchannel_clone()
        return
    logging.debug("Got args=%s %d" % (args, len(args)))
    # allow globbing of configchannel names
    ccs = filter_results(self.do_configchannel_list('', True), args)
    logging.debug("Filtered ccs %s" % ccs)
    for cc in ccs:
        logging.debug("Cloning %s" % cc)
        ccdetails = self.export_configchannel_getdetails(cc)

        # If the -x/--regex option is passed, do a sed-style replacement over
        # the name, label and description.  This makes it easier to clone when
        # content is based on a known naming convention
        if options.regex:
            # Expect option to be formatted like a sed-replacement, s/foo/bar
            findstr = options.regex.split("/")[1]
            replacestr = options.regex.split("/")[2]
            logging.debug("--regex selected with %s, replacing %s with %s" %
                          (options.regex, findstr, replacestr))

            newname = re.sub(findstr, replacestr, ccdetails['name'])
            ccdetails['name'] = newname
            newlabel = re.sub(findstr, replacestr, ccdetails['label'])
            ccdetails['label'] = newlabel
            newdesc = re.sub(findstr, replacestr, ccdetails['description'])
            ccdetails['description'] = newdesc
            logging.debug("regex mode : %s %s %s" % (ccdetails['name'],
                                                     ccdetails['label'], ccdetails['description']))
        elif options.clonelabel:
            if len(ccs) > 1:
                newlabel = options.clonelabel + ccdetails['label']
                ccdetails['label'] = newlabel
                newname = options.clonelabel + ccdetails['name']
                ccdetails['name'] = newname
                logging.debug("clonelabel mode with >1 channel : %s" %
                              ccdetails['label'])
            else:
                newlabel = options.clonelabel
                ccdetails['label'] = newlabel
                newname = options.clonelabel
                ccdetails['name'] = newname
                logging.debug("clonelabel mode with 1 channel : %s" %
                              ccdetails['label'])

        # Finally : import the cc from the modified ccdetails
        if self.import_configchannel_fromdetails(ccdetails) != True:
            logging.error("Failed to clone %s to %s" %
                          (cc, ccdetails['label']))

####################
# configchannel helper


def is_configchannel(self, name):
    if not name:
        return
    return name in self.do_configchannel_list(name, True)


def check_configchannel(self, name):
    if not name:
        logging.error("no configchannel given")
        return False
    if not self.is_configchannel(name):
        logging.error("invalid configchannel label " + name)
        return False
    return True


def dump_configchannel_filedetails(self, name, filename):
    content = self.do_configchannel_filedetails(name + " " + filename)
    return content


def dump_configchannel(self, name, replacedict=None, excludes=None):
    if not excludes:
        excludes = ["Revision:", "Created:", "Modified:"]
    content = self.do_configchannel_details(name)

    for filename in self.do_configchannel_listfiles(name, True):
        content.extend(self.dump_configchannel_filedetails(name, filename))

    content = get_normalized_text(content, replacedict=replacedict, excludes=excludes)

    return content

####################


def help_configchannel_diff(self):
    print('configchannel_diff: diff between config channels')
    print('')
    print('usage: configchannel_diff SOURCE_CHANNEL TARGET_CHANNEL')


def complete_configchannel_diff(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_configchannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_configchannel_list('', True), text)
    return []


def do_configchannel_diff(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_configchannel_diff()
        return

    source_channel = args[0]
    if not self.check_configchannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_configchannel_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_configchannel_getcorresponding(source_channel)
    if not self.check_configchannel(target_channel):
        return

    source_replacedict, target_replacedict = get_string_diff_dicts(source_channel, target_channel)

    source_data = self.dump_configchannel(source_channel, source_replacedict)
    target_data = self.dump_configchannel(target_channel, target_replacedict)

    return diff(source_data, target_data, source_channel, target_channel)

####################


def help_configchannel_sync(self):
    print('configchannel_sync:')
    print('sync config files between two config channels')
    print('')
    print('usage: configchannel_sync SOURCE_CHANNEL TARGET_CHANNEL')


def complete_configchannel_sync(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_configchannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_configchannel_list('', True), text)
    return []


def do_configchannel_sync(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_configchannel_sync()
        return

    source_channel = args[0]
    if not self.check_configchannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_configchannel_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_configchannel_getcorresponding(source_channel)
    if not self.check_configchannel(target_channel):
        return

    logging.info("syncing files from configchannel " + source_channel + " to " + target_channel)

    source_files = set(self.do_configchannel_listfiles(source_channel, doreturn=True))
    target_files = set(self.do_configchannel_listfiles(target_channel, doreturn=True))

    both = source_files & target_files
    if both:
        print("files common in both channels:")
        print("\n".join(both))
        print('')

    source_only = source_files.difference(target_files)
    if source_only:
        print("files only in source " + source_channel)
        print("\n".join(source_only))
        print('')

    target_only = target_files.difference(source_files)
    if target_only:
        print("files only in target " + target_channel)
        print("\n".join(target_only))
        print('')

    if both:
        print("files that are in both channels will be overwritten in the target channel")
    if source_only:
        print("files only in the source channel will be added to the target channel")
    if target_only:
        print("files only in the target channel will be deleted")

    if not (both or source_only or target_only):
        logging.info("nothing to do")
        return

    if not self.user_confirm('perform synchronisation [y/N]:'):
        return

    source_data_list = self.client.configchannel.lookupFileInfo(
        self.session, source_channel,
        list(both) + list(source_only))

    for source_data in source_data_list:
        if source_data.get('type') == 'file' or source_data.get('type') == 'directory':
            if source_data.get('contents') and not source_data.get('binary'):
                contents = source_data.get('contents').encode('base64')
            else:
                contents = source_data.get('contents')
            target_data = {
                'contents':                 contents,
                'contents_enc64':           True,
                'owner':                    source_data.get('owner'),
                'group':                    source_data.get('group'),
                # get permissions from permissions_mode instead of permissions
                'permissions':              source_data.get('permissions_mode'),
                'selinux_ctx':              source_data.get('selinux_ctx'),
                'macro-start-delimiter':    source_data.get('macro-start-delimiter'),
                'macro-end-delimiter':      source_data.get('macro-end-delimiter'),
            }
            for k, v in target_data.items():
                if not v:
                    del target_data[k]
            if source_data.get('type') == 'directory':
                del target_data['contents_enc64']
            logging.debug(source_data.get('path') + ": " + str(target_data))
            self.client.configchannel.createOrUpdatePath(self.session,
                                                         target_channel,
                                                         source_data.get('path'),
                                                         source_data.get('type') == 'directory',
                                                         target_data)

        elif source_data.get('type') == 'symlink':
            target_data = {
                'target_path':  source_data.get('target_path'),
                'selinux_ctx':  source_data.get('selinux_ctx'),
            }
            logging.debug(source_data.get('path') + ": " + str(target_data))
            self.client.configchannel.createOrUpdateSymlink(self.session,
                                                            target_channel,
                                                            source_data.get('path'),
                                                            target_data)

        else:
            logging.warning("unknown file type " + source_data.type)

    # removing all files from target channel that did not exist on source channel
    if target_only:
        #self.do_configchannel_removefiles( target_channel + " " + "/.metainfo" + " ".join(target_only) )
        self.do_configchannel_removefiles(target_channel + " " + " ".join(target_only))
07070100000018000081B40000000000000000000000015DA8415F0000178C000000000000000000000000000000000000002300000000spacecmd/src/spacecmd/cryptokey.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_cryptokey_create(self):
    print('cryptokey_create: Create a cryptographic key')
    print('''usage: cryptokey_create [options])

options:
  -t GPG or SSL
  -d DESCRIPTION
  -f KEY_FILE''')


def do_cryptokey_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-t', '--type')
    arg_parser.add_argument('-d', '--description')
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)
    options.contents = None

    if is_interactive(options):
        options.type = prompt_user('GPG or SSL [G/S]:')

        options.description = ''
        while options.description == '':
            options.description = prompt_user('Description:')

        if self.user_confirm('Read an existing file [y/N]:',
                             nospacer=True, ignore_yes=True):
            options.file = prompt_user('File:')
        else:
            options.contents = editor(delete=True)
    else:
        if not options.type:
            logging.error('The key type is required')
            return

        if not options.description:
            logging.error('A description is required')
            return

        if not options.file:
            logging.error('A file containing the key is required')
            return

    # read the file the user specified
    if options.file:
        options.contents = read_file(options.file)

    if not options.contents:
        logging.error('No contents of the file')
        return

    # translate the key type to what the server expects
    if re.match('G', options.type, re.I):
        options.type = 'GPG'
    elif re.match('S', options.type, re.I):
        options.type = 'SSL'
    else:
        logging.error('Invalid key type')
        return

    self.client.kickstart.keys.create(self.session,
                                      options.description,
                                      options.type,
                                      options.contents)

####################


def help_cryptokey_delete(self):
    print('cryptokey_delete: Delete a cryptographic key')
    print('usage: cryptokey_delete NAME')


def complete_cryptokey_delete(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_cryptokey_list('', True),
                             text)


def do_cryptokey_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_cryptokey_delete()
        return

    # allow globbing of cryptokey names
    keys = filter_results(self.do_cryptokey_list('', True), args)
    logging.debug("cryptokey_delete called with args %s, keys=%s" %
                  (args, keys))

    if not keys:
        logging.error("No keys matched argument %s" % args)
        return

    # Print the keys prior to the confirmation
    print('\n'.join(sorted(keys)))

    if self.user_confirm('Delete key(s) [y/N]:'):
        for key in keys:
            self.client.kickstart.keys.delete(self.session, key)

####################


def help_cryptokey_list(self):
    print('cryptokey_list: List all cryptographic keys (SSL, GPG)')
    print('usage: cryptokey_list')


def do_cryptokey_list(self, args, doreturn=False):
    keys = self.client.kickstart.keys.listAllKeys(self.session)
    keys = [k.get('description') for k in keys]

    if doreturn:
        return keys
    else:
        if keys:
            print('\n'.join(sorted(keys)))

####################


def help_cryptokey_details(self):
    print('cryptokey_details: Show the contents of a cryptographic key')
    print('usage: cryptokey_details KEY ...')


def complete_cryptokey_details(self, text, line, beg, end):
    return tab_completer(self.do_cryptokey_list('', True), text)


def do_cryptokey_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_cryptokey_details()
        return

    # allow globbing of cryptokey names
    keys = filter_results(self.do_cryptokey_list('', True), args)
    logging.debug("cryptokey_details called with args %s, keys=%s" %
                  (args, keys))

    if not keys:
        logging.error("No keys matched argument %s" % args)
        return

    add_separator = False

    for key in keys:
        try:
            details = self.client.kickstart.keys.getDetails(self.session,
                                                            key)
        except xmlrpclib.Fault:
            logging.warning('%s is not a valid crypto key' % key)
            return

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Description: %s' % details.get('description'))
        print('Type:        %s' % details.get('type'))

        print('')
        print(details.get('content'))
07070100000019000081B40000000000000000000000015DA8415F00001619000000000000000000000000000000000000002400000000spacecmd/src/spacecmd/custominfo.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

from spacecmd.utils import *


def help_custominfo_createkey(self):
    print('custominfo_createkey: Create a custom key')
    print('usage: custominfo_createkey [NAME] [DESCRIPTION]')


def do_custominfo_createkey(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        key = args[0]
    else:
        key = ''

    while key == '':
        key = prompt_user('Name:')

    if len(args) > 1:
        description = ' '.join(args[1:])
    else:
        description = prompt_user('Description:')
        if description == '':
            description = key

    self.client.system.custominfo.createKey(self.session,
                                            key,
                                            description)

####################


def help_custominfo_deletekey(self):
    print('custominfo_deletekey: Delete a custom key')
    print('usage: custominfo_deletekey KEY ...')


def complete_custominfo_deletekey(self, text, line, beg, end):
    return tab_completer(self.do_custominfo_listkeys('', True), text)


def do_custominfo_deletekey(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_custominfo_deletekey()
        return

    # allow globbing of custominfo key names
    keys = filter_results(self.do_custominfo_listkeys('', True), args)
    logging.debug("customkey_deletekey called with args %s, keys=%s" %
                  (args, keys))

    if not keys:
        logging.error("No keys matched argument %s" % args)
        return

    # Print the keys prior to the confirmation
    print('\n'.join(sorted(keys)))

    if not self.user_confirm('Delete these keys [y/N]:'):
        return

    for key in keys:
        self.client.system.custominfo.deleteKey(self.session, key)

####################


def help_custominfo_listkeys(self):
    print('custominfo_listkeys: List all custom keys')
    print('usage: custominfo_listkeys')


def do_custominfo_listkeys(self, args, doreturn=False):
    keys = self.client.system.custominfo.listAllKeys(self.session)
    keys = [k.get('label') for k in keys]

    if doreturn:
        return keys
    else:
        if keys:
            print('\n'.join(sorted(keys)))

####################


def help_custominfo_details(self):
    print('custominfo_details: Show the details of a custom key')
    print('usage: custominfo_details KEY ...')


def complete_custominfo_details(self, text, line, beg, end):
    return tab_completer(self.do_custominfo_listkeys('', True), text)


def do_custominfo_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_custominfo_details()
        return

    # allow globbing of custominfo key names
    keys = filter_results(self.do_custominfo_listkeys('', True), args)
    logging.debug("customkey_details called with args: '{}', keys: '{}'.".format(
        ", ".join(args), ", ".join(keys)))

    if not keys:
        logging.error("No keys matched argument '{}'.".format(", ".join(args)))
        return

    add_separator = False

    all_keys = self.client.system.custominfo.listAllKeys(self.session)

    for key in keys:
        details = {}
        for key_details in all_keys:
            if key_details.get('label') == key:
                details = key_details

        if details:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True
            print('Label:        %s' % (details.get('label') or "N/A"))
            print('Description:  %s' % (details.get('description') or "N/A"))
            print('Modified:     %s' % (details.get('last_modified') or "N/A"))
            print('System Count: %i' % (details.get('system_count') or 0))

####################


def help_custominfo_updatekey(self):
    print('custominfo_updatekey: Update a custom key')
    print('usage: custominfo_updatekey [NAME] [DESCRIPTION]')


def do_custominfo_updatekey(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        key = args[0]
    else:
        key = ''

    while key == '':
        key = prompt_user('Name:')

    if len(args) > 1:
        description = ' '.join(args[1:])
    else:
        description = prompt_user('Description:')
        if description == '':
            description = key

    self.client.system.custominfo.updateKey(self.session,
                                            key,
                                            description)
0707010000001A000081B40000000000000000000000015DA8415F0000234B000000000000000000000000000000000000002600000000spacecmd/src/spacecmd/distribution.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from spacecmd.utils import *


def help_distribution_create(self):
    print('distribution_create: Create a Kickstart tree')
    print('''usage: distribution_create [options]

options:
  -n NAME
  -p path to tree
  -b base channel to associate with
  -t install type [fedora|rhel_4/5/6|generic_rpm]''')


def do_distribution_create(self, args, update=False):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-p', '--path')
    arg_parser.add_argument('-b', '--base-channel')
    arg_parser.add_argument('-t', '--install-type')

    (args, options) = parse_command_arguments(args, arg_parser)

    # fill in the name of the distribution when updating
    if update:
        if args:
            options.name = args[0]
        elif not options.name:
            logging.error('The name of the distribution is required')
            return

    if is_interactive(options):
        if not update:
            options.name = prompt_user('Name:', noblank=True)

        options.path = prompt_user('Path to Kickstart Tree:', noblank=True)

        options.base_channel = ''
        while options.base_channel == '':
            print('')
            print('Base Channels')
            print('-------------')
            print('\n'.join(sorted(self.list_base_channels())))
            print('')

            options.base_channel = prompt_user('Base Channel:')

        if options.base_channel not in self.list_base_channels():
            logging.warning('Invalid channel label')
            options.base_channel = ''

        install_types = \
            self.client.kickstart.tree.listInstallTypes(self.session)

        install_types = [t.get('label') for t in install_types]

        options.install_type = ''
        while options.install_type == '':
            print('')
            print('Install Types')
            print('-------------')
            print('\n'.join(sorted(install_types)))
            print('')

            options.install_type = prompt_user('Install Type:')

            if options.install_type not in install_types:
                logging.warning('Invalid install type')
                options.install_type = ''
    else:
        if not options.name:
            logging.error('A name is required')
            return

        if not options.path:
            logging.error('A path is required')
            return

        if not options.base_channel:
            logging.error('A base channel is required')
            return

        if not options.install_type:
            logging.error('An install type is required')
            return

    if update:
        self.client.kickstart.tree.update(self.session,
                                          options.name,
                                          options.path,
                                          options.base_channel,
                                          options.install_type)
    else:
        self.client.kickstart.tree.create(self.session,
                                          options.name,
                                          options.path,
                                          options.base_channel,
                                          options.install_type)

####################


def help_distribution_list(self):
    print('distribution_list: List the available autoinstall trees')
    print('usage: distribution_list')

def do_distribution_list(self, args, doreturn=False):
    channels = self.client.kickstart.listAutoinstallableChannels(self.session)

    avail_trees = []
    for c in channels:
        trees = self.client.kickstart.tree.list(self.session,
                                                c.get('label'))
        for t in trees:
            label = t.get('label')
            if label not in avail_trees:
                avail_trees.append(label)

    if doreturn:
        return avail_trees
    else:
        if avail_trees:
            print('\n'.join(sorted(avail_trees)))

####################


def help_distribution_delete(self):
    print('distribution_delete: Delete a Kickstart tree')
    print('usage: distribution_delete LABEL')


def complete_distribution_delete(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_distribution_list('', True),
                             text)


def do_distribution_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_distribution_delete()
        return

    # allow globbing of distribution names
    dists = filter_results(self.do_distribution_list('', True), args)
    logging.debug("distribution_delete called with args %s, dists=%s" %
                  (args, dists))

    if not dists:
        logging.error("No distributions matched argument %s" % args)
        return

    # Print the distributions prior to the confirmation
    print('\n'.join(sorted(dists)))

    if self.user_confirm('Delete distribution tree(s) [y/N]:'):
        for d in dists:
            self.client.kickstart.tree.delete(self.session, d)

####################


def help_distribution_details(self):
    print('distribution_details: Show the details of a Kickstart tree')
    print('usage: distribution_details LABEL')


def complete_distribution_details(self, text, line, beg, end):
    return tab_completer(self.do_distribution_list('', True), text)


def do_distribution_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_distribution_details()
        return

    # allow globbing of distribution names
    dists = filter_results(self.do_distribution_list('', True), args)
    logging.debug("distribution_details called with args %s, dists=%s" %
                  (args, dists))

    if not dists:
        logging.error("No distributions matched argument %s" % args)
        return

    add_separator = False

    for label in dists:
        details = self.client.kickstart.tree.getDetails(self.session, label)

        channel = \
            self.client.channel.software.getDetails(self.session,
                                                    details.get('channel_id'))

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Name:    %s' % details.get('label'))
        print('Path:    %s' % details.get('abs_path'))
        print('Channel: %s' % channel.get('label'))

####################


def help_distribution_rename(self):
    print('distribution_rename: Rename a Kickstart tree')
    print('usage: distribution_rename OLDNAME NEWNAME')


def complete_distribution_rename(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_distribution_list('', True),
                             text)


def do_distribution_rename(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_distribution_rename()
        return

    oldname = args[0]
    newname = args[1]

    self.client.kickstart.tree.rename(self.session, oldname, newname)

####################


def help_distribution_update(self):
    print('distribution_update: Update the path of a Kickstart tree')
    print('''usage: distribution_update NAME [options]

options:
  -p path to tree
  -b base channel to associate with
  -t install type [fedora|rhel_4/5/6|generic_rpm]''')


def complete_distribution_update(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_distribution_list('', True),
                             text)


def do_distribution_update(self, args):
    arguments, _ = parse_command_arguments(args, get_argument_parser())
    if not arguments:
        self.help_distribution_update()
    else:
        return self.do_distribution_create(args, update=True)
0707010000001B000081B40000000000000000000000015DA8415F000045BB000000000000000000000000000000000000002000000000spacecmd/src/spacecmd/errata.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2013--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from operator import itemgetter
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib

from spacecmd.utils import *


def help_errata_list(self):
    print('errata_list: List all patches')
    print('usage: errata_list')


def do_errata_list(self, args, doreturn=False):
    self.generate_errata_cache()

    if doreturn:
        return self.all_errata.keys()
    else:
        if self.all_errata.keys():
            print('\n'.join(sorted(self.all_errata.keys())))

####################


def help_errata_summary(self):
    print('errata_summary: Print a summary of all errata')
    print('usage: errata_summary')


def do_errata_summary(self, args):
    self.generate_errata_cache()

    map(print_errata_summary, sorted(self.all_errata.values(),
                                     key=itemgetter('advisory_name')))

####################


def help_errata_apply(self):
    print('errata_apply: Apply an erratum to all affected systems')
    print('''usage: errata_apply [options] ERRATA|search:XXX ...)

options:
  -s START_TIME''')
    print('')
    print(self.HELP_TIME_OPTS)


def complete_errata_apply(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_apply(self, args, only_systems=None):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)
    only_systems = only_systems or []

    if not args:
        self.help_errata_apply()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # allow globbing and searching via arguments
    errata_list = self.expand_errata(args)

    systems = []
    summary = []
    to_apply_by_name = {}
    for erratum in errata_list:
        try:
            # get the systems affected by each errata
            affected_systems = \
                self.client.errata.listAffectedSystems(self.session, erratum)

            # build a list of systems that we will schedule errata for,
            # indexed by errata name
            for system in affected_systems:
                # add this system to the list of systems affected by
                # this erratum if we were not passed a list of systems
                # (and therefore all systems are to be touched) or we were
                # passed a list of systems and this one is part of that list
                if not only_systems or system.get('name') in only_systems:
                    if erratum not in to_apply_by_name:
                        to_apply_by_name[erratum] = []
                    if system.get('name') not in to_apply_by_name[erratum]:
                        to_apply_by_name[erratum].append(system.get('name'))
        except xmlrpclib.Fault:
            logging.debug('%s does not affect any systems' % erratum)
            continue

        # make a summary list to show the user
        if erratum in to_apply_by_name:
            summary.append('%s        %s' % (erratum.ljust(15),
                                             str(len(to_apply_by_name[erratum])).rjust(3)))
        else:
            logging.debug('%s does not affect any systems' % erratum)

    # get a unique list of all systems we need to touch
    for systemlist in to_apply_by_name.values():
        systems += systemlist
    systems = list(set(systems))

    if not systems:
        logging.warning('No patches to apply')
        return

    # a summary of which errata we're going to apply
    print('Errata             Systems')
    print('--------------     -------')
    print('\n'.join(sorted(summary)))
    print('')
    print('Start Time: %s' % options.start_time)

    if not self.user_confirm('Apply these patches [y/N]:'):
        return

    # if the API supports it, try to schedule multiple systems for one erratum
    # in order to reduce the number of actions scheduled
    if self.check_api_version('10.11'):
        to_apply = {}

        for system in systems:
            system_id = self.get_system_id(system)

            # only attempt to schedule unscheduled errata
            system_errata = self.client.system.getUnscheduledErrata(self.session,
                                                                    system_id)

            # make a list of systems for each erratum
            for erratum in system_errata:
                erratum_id = erratum.get('id')

                if erratum.get('advisory_name') in errata_list:
                    if erratum_id not in to_apply:
                        to_apply[erratum_id] = []

                    to_apply[erratum_id].append(system_id)

        # apply the errata
        for erratum in to_apply:
            self.client.system.scheduleApplyErrata(self.session,
                                                   to_apply[erratum],
                                                   [erratum],
                                                   options.start_time)

            logging.info('Scheduled %i system(s) for %s' %
                         (len(to_apply[erratum]),
                          self.get_erratum_name(erratum)))
    else:
        for system in systems:
            system_id = self.get_system_id(system)

            # only schedule unscheduled errata
            system_errata = self.client.system.getUnscheduledErrata(self.session,
                                                                    system_id)

            # if an errata specified for installation is unscheduled for
            # this system, add it to the list to schedule
            errata_to_apply = []
            for erratum in errata_list:
                for e in system_errata:
                    if erratum == e.get('advisory_name'):
                        errata_to_apply.append(e.get('id'))
                        break

            if not errata_to_apply:
                logging.warning('No patches to schedule for %s' % system)
                continue

            # this results in one action per erratum for each server
            self.client.system.scheduleApplyErrata(self.session,
                                                   system_id,
                                                   errata_to_apply,
                                                   options.start_time)

            logging.info('Scheduled %i patches for %s' %
                         (len(errata_to_apply), system))

####################


def help_errata_listaffectedsystems(self):
    print('errata_listaffectedsystems: List of systems affected by an patch')
    print('usage: errata_listaffectedsystems ERRATA|search:XXX ...')


def complete_errata_listaffectedsystems(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_listaffectedsystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_listaffectedsystems()
        return

    # allow globbing and searching via arguments
    errata_list = self.expand_errata(args)

    add_separator = False

    for erratum in errata_list:
        systems = self.client.errata.listAffectedSystems(self.session, erratum)

        if systems:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            print('%s:' % erratum)
            print('\n'.join(sorted([s.get('name') for s in systems])))

####################


def help_errata_listcves(self):
    print('errata_listcves: List of CVEs addressed by an patch')
    print('usage: errata_listcves ERRATA|search:XXX ...')


def complete_errata_listcves(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_listcves(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_listcves()
        return

    # allow globbing and searching via arguments
    errata_list = self.expand_errata(args)

    add_separator = False

    for erratum in errata_list:
        cves = self.client.errata.listCves(self.session, erratum)

        if cves:
            if len(errata_list) > 1:
                if add_separator:
                    print(self.SEPARATOR)
                add_separator = True

                print('%s:' % erratum)

            print('\n'.join(sorted(cves)))

####################


def help_errata_findbycve(self):
    print('errata_findbycve: List errata addressing a CVE')
    print('usage: errata_findbycve CVE-YYYY-NNNN ...')


def complete_errata_findbycve(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_findbycve(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_findbycve()
        return

    # More than one CVE may be specified
    cve_list = args
    logging.debug("Got CVE list %s" % cve_list)

    add_separator = False

    # Then iterate over the requested CVEs and dump the errata which match
    for c in cve_list:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print("%s:" % c)
        errata = self.client.errata.findByCve(self.session, c)
        if errata:
            for e in errata:
                print("%s" % e.get('advisory_name'))

####################


def help_errata_details(self):
    print('errata_details: Show the details of an patch')
    print('usage: errata_details ERRATA|search:XXX ...')


def complete_errata_details(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_details()
        return

    # allow globbing and searching via arguments
    errata_list = self.expand_errata(args)

    add_separator = False

    for erratum in errata_list:
        try:
            details = self.client.errata.getDetails(self.session, erratum)

            packages = self.client.errata.listPackages(self.session, erratum)

            systems = self.client.errata.listAffectedSystems(self.session,
                                                             erratum)

            cves = self.client.errata.listCves(self.session, erratum)

            channels = \
                self.client.errata.applicableToChannels(self.session, erratum)
        except xmlrpclib.Fault:
            logging.warning('%s is not a valid erratum' % erratum)
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Name:       %s' % erratum)
        print('Product:    %s' % details.get('product'))
        print('Type:       %s' % details.get('type'))
        print('Issue Date: %s' % details.get('issue_date'))
        print('')
        print('Topic')
        print('-----')
        print('\n'.join(wrap(details.get('topic'))))
        print('')
        print('Description')
        print('-----------')
        print('\n'.join(wrap(details.get('description'))))

        if details.get('notes'):
            print('')
            print('Notes')
            print('-----')
            print('\n'.join(wrap(details.get('notes'))))

        print('')
        print('CVEs')
        print('----')
        print('\n'.join(sorted(cves)))
        print('')
        print('Solution')
        print('--------')
        print('\n'.join(wrap(details.get('solution'))))
        print('')
        print('References')
        print('----------')
        print('\n'.join(wrap(details.get('references'))))
        print('')
        print('Affected Channels')
        print('-----------------')
        print('\n'.join(sorted([c.get('label') for c in channels])))
        print('')
        print('Affected Systems')
        print('----------------')
        print(str(len(systems)))
        print('')
        print('Affected Packages')
        print('-----------------')
        print('\n'.join(sorted(build_package_names(packages))))

####################


def help_errata_delete(self):
    print('errata_delete: Delete an patch')
    print('usage: errata_delete ERRATA|search:XXX ...')


def complete_errata_delete(self, text, line, beg, end):
    return self.tab_complete_errata(text)


def do_errata_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_delete()
        return

    # allow globbing and searching via arguments
    errata = self.expand_errata(args)

    if not errata:
        logging.warning('No patches to delete')
        return

    print('Erratum            Channels')
    print('-------            --------')

    # tell the user how many channels each erratum affects
    for erratum in sorted(errata):
        channels = self.client.errata.applicableToChannels(self.session, erratum)
        print('%s    %s' % (erratum.ljust(20), str(len(channels)).rjust(3)))

    if not self.user_confirm('Delete these patches [y/N]:'):
        return

    for erratum in errata:
        self.client.errata.delete(self.session, erratum)

    logging.info('Deleted %i patches' % len(errata))

    self.generate_errata_cache(True)

####################


def help_errata_publish(self):
    print('errata_publish: Publish an patch to a channel')
    print('usage: errata_publish ERRATA|search:XXX <CHANNEL ...>')


def complete_errata_publish(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_errata(text)
    elif len(parts) > 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)


def do_errata_publish(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_errata_publish()
        return

    # allow globbing and searching via arguments
    errata = self.expand_errata(args[0])

    channels = args[1:]

    if not errata:
        logging.warning('No patches to publish')
        return

    print('\n'.join(sorted(errata)))

    if not self.user_confirm('Publish these patches [y/N]:'):
        return

    for erratum in errata:
        self.client.errata.publish(self.session, erratum, channels)

####################


def help_errata_search(self):
    print('errata_search: List patches that meet the given criteria')
    print('usage: errata_search CVE|RHSA|RHBA|RHEA|CLA ...')
    print('')
    print('Example:')
    print('> errata_search CVE-2009:1674')
    print('> errata_search RHSA-2009:1674')


def complete_errata_search(self, text, line, beg, end):
    return tab_completer(self.do_errata_list('', True), text)


def do_errata_search(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_errata_search()
        return

    add_separator = False

    for query in args:
        errata = []

        if re.match('CVE', query, re.I):
            errata = self.client.errata.findByCve(self.session,
                                                  query.upper())
        else:
            self.generate_errata_cache()

            for name in self.all_errata.keys():
                if re.search(query, name, re.I) or \
                   re.search(query, self.all_errata[name]['advisory_synopsis'],
                             re.I):

                    match = self.all_errata[name]

                    # build a structure to pass to print_errata_summary()
                    errata.append({'advisory_name': name,
                                   'advisory_type': match['advisory_type'],
                                   'advisory_synopsis': match['advisory_synopsis'],
                                   'date': match['date']})

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if errata:
            if doreturn:
                return [erratum['advisory_name'] for erratum in errata]
            else:
                map(print_errata_summary, sorted(errata, reverse=True))
        else:
            return []
0707010000001C000081B40000000000000000000000015DA8415F00001070000000000000000000000000000000000000002A00000000spacecmd/src/spacecmd/filepreservation.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from spacecmd.utils import *


def help_filepreservation_list(self):
    print('filepreservation_list: List all file preservations')
    print('usage: filepreservation_list')


def do_filepreservation_list(self, args, doreturn=False):
    lists = [l.get('name') for l in self.client.kickstart.filepreservation.listAllFilePreservations(self.session)]

    if not doreturn:
        print('\n'.join(sorted(lists)))
    else:
        return lists

####################


def help_filepreservation_create(self):
    print('filepreservation_create: Create a file preservation list')
    print('usage: filepreservation_create [NAME] [FILE ...]')


def do_filepreservation_create(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        name = args[0]
    else:
        name = prompt_user('Name:', noblank=True)

    if len(args) > 1:
        files = args[1:]
    else:
        files = []

        while True:
            print('File List')
            print('---------')
            print('\n'.join(sorted(files)))
            print('')

            userinput = prompt_user('File [blank to finish]:')

            if userinput == '':
                break
            else:
                if userinput not in files:
                    files.append(userinput)

    print('')
    print('File List')
    print('---------')
    print('\n'.join(sorted(files)))

    if not self.user_confirm():
        return

    self.client.kickstart.filepreservation.create(self.session,
                                                  name,
                                                  files)

####################


def help_filepreservation_delete(self):
    print('filepreservation_delete: Delete a file preservation list')
    print('usage: filepreservation_delete NAME')


def complete_filepreservation_delete(self, text, line, beg, end):
    return tab_completer(self.do_filepreservation_list('', True), text)


def do_filepreservation_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_filepreservation_delete()
        return

    name = args[0]

    if not self.user_confirm('Delete this list [y/N]:'):
        return

    self.client.kickstart.filepreservation.delete(self.session, name)

####################


def help_filepreservation_details(self):
    print('''filepreservation_details: Show the details of a file
'preservation list''')
    print('usage: filepreservation_details NAME')


def complete_filepreservation_details(self, text, line, beg, end):
    return tab_completer(self.do_filepreservation_list('', True), text)


def do_filepreservation_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_filepreservation_details()
        return

    name = args[0]

    details = \
        self.client.kickstart.filepreservation.getDetails(self.session,
                                                          name)

    print('\n'.join(sorted(details.get('file_names'))))
0707010000001D000081B40000000000000000000000015DA8415F00003290000000000000000000000000000000000000001F00000000spacecmd/src/spacecmd/group.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import os
import re
import shlex
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_group_addsystems(self):
    print('group_addsystems: Add systems to a group')
    print('usage: group_addsystems GROUP <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_group_addsystems(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_group_list('', True), text)
    elif len(parts) > 2:
        return self.tab_complete_systems(parts[-1])


def do_group_addsystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_addsystems()
        return

    group_name = args.pop(0)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    system_ids = []
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue
        system_ids.append(system_id)

    self.client.systemgroup.addOrRemoveSystems(self.session,
                                               group_name,
                                               system_ids,
                                               True)

####################


def help_group_removesystems(self):
    print('group_removesystems: Remove systems from a group')
    print('usage: group_removesystems GROUP <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_group_removesystems(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_group_list('', True), text)
    elif len(parts) > 2:
        return self.tab_complete_systems(parts[-1])


def do_group_removesystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_removesystems()
        return

    group_name = args.pop(0)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    system_ids = []
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue
        system_ids.append(system_id)

    print('Systems')
    print('-------')
    print('\n'.join(sorted(systems)))

    if not self.user_confirm('Remove these systems [y/N]:'):
        return

    self.client.systemgroup.addOrRemoveSystems(self.session,
                                               group_name,
                                               system_ids,
                                               False)

####################


def help_group_create(self):
    print('group_create: Create a system group')
    print('usage: group_create [NAME] [DESCRIPTION]')


def do_group_create(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        name = args[0]
    else:
        name = prompt_user('Name:')

    if len(args) > 1:
        description = ' '.join(args[1:])
    else:
        description = prompt_user('Description:')

    self.client.systemgroup.create(self.session, name, description)

####################


def help_group_delete(self):
    print('group_delete: Delete a system group')
    print('usage: group_delete NAME ...')


def complete_group_delete(self, text, line, beg, end):
    return tab_completer(self.do_group_list('', True), text)


def do_group_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_delete()
        return

    groups = args

    self.do_group_details('', True)
    if not self.user_confirm('Delete these groups [y/N]:'):
        return

    for group in groups:
        self.client.systemgroup.delete(self.session, group)

####################


def help_group_backup(self):
    print('group_backup: backup a system group')
    print('''usage: group_backup NAME [OUTDIR])

OUTDIR defaults to $HOME/spacecmd-backup/group/YYYY-MM-DD/NAME
''')


def complete_group_backup(self, text, line, beg, end):
    List = self.do_group_list('', True)
    List.append('ALL')
    return tab_completer(List, text)


def do_group_backup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_backup()
        return

    groups = args
    if len(args) == 1 and args[0] == 'ALL':
        groups = self.do_group_list('', True)

    outputpath_base = None

    # use an output base from the user if it was passed
    if len(args) == 2:
        outputpath_base = datetime.now().strftime(os.path.expanduser(args[1]))
    else:
        outputpath_base = os.path.expanduser('~/spacecmd-backup/group')

        # make the final output path be <base>/date
        outputpath_base = os.path.join(outputpath_base,
                                       datetime.now().strftime("%Y-%m-%d"))

    try:
        if not os.path.isdir(outputpath_base):
            os.makedirs(outputpath_base)
    except OSError:
        logging.error('Could not create output directory')
        return

    for group in groups:
        print("Backup Group: %s" % group)
        details = self.client.systemgroup.getDetails(self.session, group)
        outputpath = outputpath_base + "/" + group
        print("Output File: %s" % outputpath)
        fh = open(outputpath, 'w')
        fh.write(details['description'])
        fh.close()

####################


def help_group_restore(self):
    print('group_restore: restore a system group')
    print('usage: group_restore INPUTDIR [NAME] ...')


def complete_group_restore(self, text, line, beg, end):
    parts = shlex.split(line)

    if len(parts) > 1:
        groups = self.do_group_list('', True)
        groups.append('ALL')
        return tab_completer(groups, text)


def do_group_restore(self, args):
    arg_parser = get_argument_parser()

    (args, options) = parse_command_arguments(args, arg_parser)

    inputdir = os.getcwd()
    groups = []
    files = {}
    current = {}

    if args:
        inputdir = args[0]
        groups = args[1:]
    else:
        self.help_group_restore()
        return

    inputdir = os.path.abspath(inputdir)
    logging.debug("Input Directory: %s" % (inputdir))

    # make a list of file items in the input dir
    if os.path.isdir(inputdir):
        d_content = os.listdir(inputdir)
        for d_item in d_content:
            if os.path.isfile(inputdir + "/" + d_item):
                logging.debug("Found file %s" % inputdir + "/" + d_item)
                files[d_item] = inputdir + "/" + d_item
    else:
        logging.error("Restore dir %s does not exits or is not a directory" % inputdir)
        return

    if not files:
        logging.error("Restore dir %s has no restore items" % inputdir)
        return

    if (len(groups) == 1 and groups[0] == 'ALL') or not groups:
        groups = files.keys()
    elif groups:
        for group in groups:
            if group in files:
                groups.append(group)
            else:
                logging.error("Group %s was not found in backup" % (group))

    for groupname in self.do_group_list('', True):
        details = self.client.systemgroup.getDetails(self.session, groupname)
        current[groupname] = details['description']
        current[groupname] = current[groupname].rstrip('\n')

    for groupname in files:
        fh = open(files[groupname], 'r')
        details = fh.read()
        fh.close()
        details = details.rstrip('\n')

        if groupname in current and current[groupname] == details:
            logging.debug("Already have %s" % groupname)
            continue

        elif groupname in current:
            logging.debug("Already have %s but the description has changed" % groupname)

            if is_interactive(options):
                print("Changing description from:")
                print("\n\"%s\"\nto\n\"%s\"\n" % (current[groupname], details))
                userinput = prompt_user('Continue [y/N]:')

                if re.match('y', userinput, re.I):
                    logging.info("Updating description for group: %s" % groupname)
                    self.client.systemgroup.update(self.session, groupname, details)
            else:
                logging.info("Updating description for group: %s" % groupname)
                self.client.systemgroup.update(self.session, groupname, details)
        else:
            logging.info("Creating new group %s" % groupname)
            group = self.client.systemgroup.create(self.session, groupname, details)

####################


def help_group_list(self):
    print('group_list: List available system groups')
    print('usage: group_list')


def do_group_list(self, args, doreturn=False):
    groups = self.client.systemgroup.listAllGroups(self.session)
    groups = [g.get('name') for g in groups]

    if doreturn:
        return groups
    else:
        if groups:
            print('\n'.join(sorted(groups)))

####################


def help_group_listsystems(self):
    print('group_listsystems: List the members of a group')
    print('usage: group_listsystems GROUP')


def complete_group_listsystems(self, text, line, beg, end):
    return tab_completer(self.do_group_list('', True), text)


def do_group_listsystems(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_listsystems()
        return

    group = args[0]

    try:
        systems = self.client.systemgroup.listSystems(self.session, group)
        systems = [s.get('profile_name') for s in systems]
    except xmlrpclib.Fault:
        logging.warning('%s is not a valid group' % group)
        return []

    if doreturn:
        return systems
    else:
        if systems:
            print('\n'.join(sorted(systems)))

####################


def help_group_details(self):
    print('group_details: Show the details of a system group')
    print('usage: group_details GROUP ...')


def complete_group_details(self, text, line, beg, end):
    return tab_completer(self.do_group_list('', True), text)


def do_group_details(self, args, short=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_group_details()
        return

    add_separator = False

    for group in args:
        try:
            details = self.client.systemgroup.getDetails(self.session,
                                                         group)

            systems = self.client.systemgroup.listSystems(self.session,
                                                          group)

            systems = [s.get('profile_name') for s in systems]
        except xmlrpclib.Fault:
            logging.warning('%s is not a valid group' % group)
            return

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('ID:                %i' % details.get('id'))
        print('Name:              %s' % details.get('name'))
        print('Description:       %s' % details.get('description'))
        print('Number of Systems: %i' % details.get('system_count'))

        if not short:
            print('')
            print('Members')
            print('-------')
            print('\n'.join(sorted(systems)))
0707010000001E000081B40000000000000000000000015DA8415F0001577B000000000000000000000000000000000000002300000000spacecmd/src/spacecmd/kickstart.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2013--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from getpass import getpass
from operator import itemgetter
try:
    from urllib2 import urlopen, HTTPError
except ImportError:
    from urllib.request import urlopen
    from urllib.error import HTTPError
import re
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *

KICKSTART_OPTIONS = ['autostep', 'interactive', 'install', 'upgrade',
                     'text', 'network', 'cdrom', 'harddrive', 'nfs',
                     'url', 'lang', 'langsupport keyboard', 'mouse',
                     'device', 'deviceprobe', 'zerombr', 'clearpart',
                     'bootloader', 'timezone', 'auth', 'rootpw', 'selinux',
                     'reboot', 'firewall', 'xconfig', 'skipx', 'key',
                     'ignoredisk', 'autopart', 'cmdline', 'firstboot',
                     'graphical', 'iscsi', 'iscsiname', 'logging',
                     'monitor', 'multipath', 'poweroff', 'halt', 'service',
                     'shutdown', 'user', 'vnc', 'zfcp']

VIRT_TYPES = ['none', 'para_host', 'qemu', 'xenfv', 'xenpv']

UPDATE_TYPES = ['red_hat', 'all', 'none']


def help_kickstart_list(self):
    print('kickstart_list: List the available Kickstart profiles')
    print('usage: kickstart_list')


def do_kickstart_list(self, args, doreturn=False):
    kickstarts = self.client.kickstart.listKickstarts(self.session)
    kickstarts = [k.get('name') for k in kickstarts]

    if doreturn:
        return kickstarts
    else:
        if kickstarts:
            print('\n'.join(sorted(kickstarts)))

####################


def help_kickstart_create(self):
    print('kickstart_create: Create a Kickstart profile')
    print('''usage: kickstart_create [options])

options:
  -n NAME
  -d DISTRIBUTION
  -p ROOT_PASSWORD
  -v VIRT_TYPE ['none', 'para_host', 'qemu', 'xenfv', 'xenpv']''')


def do_kickstart_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-d', '--distribution')
    arg_parser.add_argument('-v', '--virt-type')
    arg_parser.add_argument('-p', '--root-password')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name:', noblank=True)

        print('Virtualization Types')
        print('--------------------')
        print('\n'.join(sorted(self.VIRT_TYPES)))
        print('')

        options.virt_type = prompt_user('Virtualization Type [none]:')
        if options.virt_type == '' or options.virt_type not in self.VIRT_TYPES:
            options.virt_type = 'none'

        options.distribution = ''
        while options.distribution == '':
            trees = self.do_distribution_list('', True)
            print('')
            print('Distributions')
            print('-------------')
            print('\n'.join(sorted(trees)))
            print('')

            options.distribution = prompt_user('Select:')

        options.root_password = ''
        while options.root_password == '':
            print('')
            password1 = getpass('Root Password: ')
            password2 = getpass('Repeat Password: ')

            if password1 == password2:
                options.root_password = password1
            elif password1 == '':
                logging.warning('Password must be at least 5 characters')
            else:
                logging.warning("Passwords don't match")
    else:
        if not options.name:
            logging.error('The Kickstart name is required')
            return

        if not options.distribution:
            logging.error('The distribution is required')
            return

        if not options.virt_type:
            options.virt_type = 'none'

        if not options.root_password:
            logging.error('A root password is required')
            return

    # leave this blank to use the default server
    host = ''

    self.client.kickstart.createProfile(self.session,
                                        options.name,
                                        options.virt_type,
                                        options.distribution,
                                        host,
                                        options.root_password)

####################


def help_kickstart_delete(self):
    print('kickstart_delete: Delete kickstart profile(s)')
    print('usage: kickstart_delete PROFILE')
    print('usage: kickstart_delete PROFILE1 PROFILE2')
    print('usage: kickstart_delete \"PROF*\"')


def complete_kickstart_delete(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 1:
        self.help_kickstart_delete()
        return

    # allow globbing of kickstart labels
    all_labels = self.do_kickstart_list('', True)
    labels = filter_results(all_labels, args)
    logging.debug("Got labels to delete of %s" % labels)

    if not labels:
        logging.error("No valid kickstart labels passed as arguments!")
        self.help_kickstart_delete()
        return

    for label in labels:
        if not label in all_labels:
            logging.error("kickstart label %s doesn't exist!" % label)
            continue

        if self.user_confirm("Delete profile %s [y/N]:" % label):
            self.client.kickstart.deleteProfile(self.session, label)

####################


def help_kickstart_import(self):
    print('kickstart_import: Import a Kickstart profile from a file')
    print('''usage: kickstart_import [options])

options:
  -f FILE
  -n NAME
  -d DISTRIBUTION
  -v VIRT_TYPE ['none', 'para_host', 'qemu', 'xenfv', 'xenpv']''')


def do_kickstart_import(self, args):
    self.kickstart_import_file(raw=False, args=args)


def kickstart_import_file(self, raw, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-d', '--distribution')
    arg_parser.add_argument('-v', '--virt-type')
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name:', noblank=True)
        options.file = prompt_user('File:', noblank=True)

        print('Virtualization Types')
        print('--------------------')
        print('\n'.join(sorted(self.VIRT_TYPES)))
        print('')

        options.virt_type = prompt_user('Virtualization Type [none]:')
        if options.virt_type == '' or options.virt_type not in self.VIRT_TYPES:
            options.virt_type = 'none'

        options.distribution = ''
        while options.distribution == '':
            trees = self.do_distribution_list('', True)
            print('')
            print('Distributions')
            print('-------------')
            print('\n'.join(sorted(trees)))
            print('')

            options.distribution = prompt_user('Select:')
    else:
        if not options.name:
            logging.error('The Kickstart name is required')
            return

        if not options.distribution:
            logging.error('The distribution is required')
            return

        if not options.file:
            logging.error('A filename is required')
            return

        if not options.virt_type:
            options.virt_type = 'none'

    # read the contents of the Kickstart file
    options.contents = read_file(options.file)

    if raw:
        self.client.kickstart.importRawFile(self.session,
                                            options.name,
                                            options.virt_type,
                                            options.distribution,
                                            options.contents)
    else:
        # use the default server
        host = ''

        self.client.kickstart.importFile(self.session,
                                         options.name,
                                         options.virt_type,
                                         options.distribution,
                                         host,
                                         options.contents)

####################


def help_kickstart_import_raw(self):
    print('kickstart_import_raw: Import a raw Kickstart or autoyast profile from a file')
    print('''usage: kickstart_import_raw [options])

options:
  -f FILE
  -n NAME
  -d DISTRIBUTION
  -v VIRT_TYPE ['none', 'para_host', 'qemu', 'xenfv', 'xenpv']''')


def do_kickstart_import_raw(self, args):
    self.kickstart_import_file(raw=True, args=args)

####################


def help_kickstart_details(self):
    print('kickstart_details: Show the details of a Kickstart profile')
    print('usage: kickstart_details PROFILE')


def complete_kickstart_details(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_kickstart_details()
        return

    label = args[0]
    kickstart = None

    profiles = self.client.kickstart.listKickstarts(self.session)
    for p in profiles:
        if p.get('label') == label:
            kickstart = p
            break

    if not kickstart:
        logging.warning('Invalid Kickstart profile')
        return

    act_keys = \
        self.client.kickstart.profile.keys.getActivationKeys(self.session,
                                                             label)

    try:
        variables = self.client.kickstart.profile.getVariables(self.session,
                                                               label)
    except xmlrpclib.Fault:
        variables = {}

    tree = \
        self.client.kickstart.tree.getDetails(self.session,
                                              kickstart.get('tree_label'))

    base_channel = \
        self.client.channel.software.getDetails(self.session,
                                                tree.get('channel_id'))

    child_channels = \
        self.client.kickstart.profile.getChildChannels(self.session,
                                                       label)

    custom_options = \
        self.client.kickstart.profile.getCustomOptions(self.session,
                                                       label)

    advanced_options = \
        self.client.kickstart.profile.getAdvancedOptions(self.session,
                                                         label)

    config_manage = \
        self.client.kickstart.profile.system.checkConfigManagement(
            self.session, label)

    remote_commands = \
        self.client.kickstart.profile.system.checkRemoteCommands(
            self.session, label)

    partitions = \
        self.client.kickstart.profile.system.getPartitioningScheme(
            self.session, label)

    crypto_keys = \
        self.client.kickstart.profile.system.listKeys(self.session,
                                                      label)

    file_preservations = \
        self.client.kickstart.profile.system.listFilePreservations(
            self.session, label)

    software = self.client.kickstart.profile.software.getSoftwareList(
        self.session, label)

    scripts = self.client.kickstart.profile.listScripts(self.session,
                                                        label)

    result = []
    result.append('Name:        %s' % kickstart.get('name'))
    result.append('Label:       %s' % kickstart.get('label'))
    result.append('Tree:        %s' % kickstart.get('tree_label'))
    result.append('Active:      %s' % kickstart.get('active'))
    result.append('Advanced:    %s' % kickstart.get('advanced_mode'))
    result.append('Org Default: %s' % kickstart.get('org_default'))

    result.append('')
    result.append('Configuration Management: %s' % config_manage)
    result.append('Remote Commands:          %s' % remote_commands)

    result.append('')
    result.append('Software Channels')
    result.append('-----------------')
    result.append(base_channel.get('label'))

    for channel in sorted(child_channels):
        result.append('  |-- %s' % channel)

    if advanced_options:
        result.append('')
        result.append('Advanced Options')
        result.append('----------------')
        for o in sorted(advanced_options, key=itemgetter('name')):
            if o.get('arguments'):
                result.append('%s %s' % (o.get('name'), o.get('arguments')))

    if custom_options:
        result.append('')
        result.append('Custom Options')
        result.append('--------------')
        for o in sorted(custom_options, key=itemgetter('arguments')):
            result.append(re.sub('\n', '', o.get('arguments')))

    if partitions:
        result.append('')
        result.append('Partitioning')
        result.append('------------')
        result.append('\n'.join(partitions))

    result.append('')
    result.append('Software')
    result.append('--------')
    result.append('\n'.join(software))

    if act_keys:
        result.append('')
        result.append('Activation Keys')
        result.append('---------------')
        for k in sorted(act_keys, key=itemgetter('key')):
            result.append(k.get('key'))

    if crypto_keys:
        result.append('')
        result.append('Crypto Keys')
        result.append('-----------')
        for k in sorted(crypto_keys, key=itemgetter('description')):
            result.append(k.get('description'))

    if file_preservations:
        result.append('')
        result.append('File Preservations')
        result.append('------------------')
        for fp in sorted(file_preservations, key=itemgetter('name')):
            result.append(fp.get('name'))
            for profile_name in sorted(fp.get('file_names')):
                result.append('    |-- %s' % profile_name)

    if variables:
        result.append('')
        result.append('Variables')
        result.append('---------')
        for k in sorted(variables.keys()):
            result.append('%s = %s' % (k, str(variables[k])))

    if scripts:
        result.append('')
        result.append('Scripts')
        result.append('-------')

        add_separator = False

        for s in scripts:
            if add_separator:
                result.append(self.SEPARATOR)
            add_separator = True

            result.append('Type:        %s' % s.get('script_type'))
            result.append('Chroot:      %s' % s.get('chroot'))

            if s.get('interpreter'):
                result.append('Interpreter: %s' % s.get('interpreter'))

            result.append('')
            result.append(s.get('contents'))

    return result

####################


def kickstart_getcontents(self, profile):

    kickstart = None
    if self.check_api_version('10.11'):
        kickstart = self.client.kickstart.profile.downloadRenderedKickstart(
            self.session, profile)
    else:
        # old versions of th API don't return a rendered Kickstart,
        # so grab it in a hacky way
        url = 'http://%s/ks/cfg/label/%s' % (self.server, profile)

        try:
            logging.debug('Retrieving %s' % url)
            response = urlopen(url)
            kickstart = response.read()
        except HTTPError:
            logging.error('Could not retrieve the Kickstart file')

    return kickstart


def help_kickstart_getcontents(self):
    print('kickstart_getcontents: Show the contents of a Kickstart profile')
    print('                   as they would be presented to a client')
    print('usage: kickstart_getcontents LABEL')


def complete_kickstart_getcontents(self, text, line, beg, end):
    return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_getcontents(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_getcontents()
        return

    profile = args[0]

    kickstart = self.kickstart_getcontents(profile)

    if kickstart:
        # We try to encode the output as UTF8, which is what we expect from
        # the API.  This avoids "'ascii' codec can't encode character" errors
        try:
            print(kickstart.encode('UTF8'))
        except UnicodeDecodeError:
            print(kickstart)

####################


def help_kickstart_rename(self):
    print('kickstart_rename: Rename a Kickstart profile')
    print('usage: kickstart_rename OLDNAME NEWNAME')


def complete_kickstart_rename(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_rename(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_kickstart_rename()
        return

    oldname = args[0]
    newname = args[1]

    self.client.kickstart.renameProfile(self.session, oldname, newname)

####################


def help_kickstart_listcryptokeys(self):
    print('kickstart_listcryptokeys: List the crypto keys associated ' +
          'with a Kickstart profile')
    print('usage: kickstart_listcryptokeys PROFILE')


def complete_kickstart_listcryptokeys(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listcryptokeys(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listcryptokeys()
        return

    profile = args[0]

    keys = self.client.kickstart.profile.system.listKeys(self.session,
                                                         profile)
    keys = [k.get('description') for k in keys]

    if doreturn:
        return keys
    else:
        if keys:
            print('\n'.join(sorted(keys)))

####################


def help_kickstart_addcryptokeys(self):
    print('kickstart_addcryptokeys: Add crypto keys to a Kickstart profile')
    print('usage: kickstart_addcryptokeys PROFILE <KEY ...>')


def complete_kickstart_addcryptokeys(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_cryptokey_list('', True), text)


def do_kickstart_addcryptokeys(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_addcryptokeys()
        return

    profile = args[0]
    keys = args[1:]

    self.client.kickstart.profile.system.addKeys(self.session,
                                                 profile,
                                                 keys)

####################


def help_kickstart_removecryptokeys(self):
    print('kickstart_removecryptokeys: Remove crypto keys from a ' +
          'Kickstart profile')
    print('usage: kickstart_removecryptokeys PROFILE <KEY ...>')


def complete_kickstart_removecryptokeys(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        # only tab complete keys currently assigned to the profile
        try:
            keys = self.do_kickstart_listcryptokeys(parts[1], True)
        except xmlrpclib.Fault:
            keys = []

        return tab_completer(keys, text)


def do_kickstart_removecryptokeys(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removecryptokeys()
        return

    profile = args[0]
    keys = args[1:]

    self.client.kickstart.profile.system.removeKeys(self.session,
                                                    profile,
                                                    keys)

####################


def help_kickstart_listactivationkeys(self):
    print('kickstart_listactivationkeys: List the activation keys ' +
          'associated with a Kickstart profile')
    print('usage: kickstart_listactivationkeys PROFILE')


def complete_kickstart_listactivationkeys(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listactivationkeys(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listactivationkeys()
        return

    profile = args[0]

    keys = \
        self.client.kickstart.profile.keys.getActivationKeys(self.session,
                                                             profile)

    keys = [k.get('key') for k in keys]

    if doreturn:
        return keys
    else:
        if keys:
            print('\n'.join(sorted(keys)))

####################


def help_kickstart_addactivationkeys(self):
    print('kickstart_addactivationkeys: Add activation keys to a ' +
          'Kickstart profile')
    print('usage: kickstart_addactivationkeys PROFILE <KEY ...>')


def complete_kickstart_addactivationkeys(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_activationkey_list('', True),
                             text)


def do_kickstart_addactivationkeys(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_addactivationkeys()
        return

    profile = args[0]
    keys = args[1:]

    for key in keys:
        self.client.kickstart.profile.keys.addActivationKey(self.session,
                                                            profile,
                                                            key)

####################


def help_kickstart_removeactivationkeys(self):
    print('kickstart_removeactivationkeys: Remove activation keys from ' +
          'a Kickstart profile')
    print('usage: kickstart_removeactivationkeys PROFILE <KEY ...>')


def complete_kickstart_removeactivationkeys(self, text, line, beg,
                                            end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        # only tab complete keys currently assigned to the profile
        try:
            keys = self.do_kickstart_listactivationkeys(parts[1], True)
        except xmlrpclib.Fault:
            keys = []

        return tab_completer(keys, text)


def do_kickstart_removeactivationkeys(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removeactivationkeys()
        return

    profile = args[0]
    keys = args[1:]

    if not self.user_confirm('Remove these keys [y/N]:'):
        return

    for key in keys:
        self.client.kickstart.profile.keys.removeActivationKey(self.session,
                                                               profile,
                                                               key)

####################


def help_kickstart_enableconfigmanagement(self):
    print('kickstart_enableconfigmanagement: Enable configuration ' +
          'management on a Kickstart profile')
    print('usage: kickstart_enableconfigmanagement PROFILE')


def complete_kickstart_enableconfigmanagement(self, text, line, beg,
                                              end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_enableconfigmanagement(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_enableconfigmanagement()
        return

    profile = args[0]

    self.client.kickstart.profile.system.enableConfigManagement(
        self.session, profile)

####################


def help_kickstart_disableconfigmanagement(self):
    print('kickstart_disableconfigmanagement: Disable configuration ' +
          'management on a Kickstart profile')
    print('usage: kickstart_disableconfigmanagement PROFILE')


def complete_kickstart_disableconfigmanagement(self, text, line, beg,
                                               end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_disableconfigmanagement(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_disableconfigmanagement()
        return

    profile = args[0]

    self.client.kickstart.profile.system.disableConfigManagement(
        self.session, profile)

####################


def help_kickstart_enableremotecommands(self):
    print('kickstart_enableremotecommands: Enable remote commands ' +
          'on a Kickstart profile')
    print('usage: kickstart_enableremotecommands PROFILE')


def complete_kickstart_enableremotecommands(self, text, line, beg,
                                            end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_enableremotecommands(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_enableremotecommands()
        return

    profile = args[0]

    self.client.kickstart.profile.system.enableRemoteCommands(self.session,
                                                              profile)

####################


def help_kickstart_disableremotecommands(self):
    print('kickstart_disableremotecommands: Disable remote commands ' +
          'on a Kickstart profile')
    print('usage: kickstart_disableremotecommands PROFILE')


def complete_kickstart_disableremotecommands(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_disableremotecommands(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_disableremotecommands()
        return

    profile = args[0]

    self.client.kickstart.profile.system.disableRemoteCommands(self.session,
                                                               profile)

####################


def help_kickstart_setlocale(self):
    print('kickstart_setlocale: Set the locale for a Kickstart profile')
    print('usage: kickstart_setlocale PROFILE LOCALE')


def complete_kickstart_setlocale(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) == 3:
        return tab_completer(list_locales(), text)


def do_kickstart_setlocale(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_kickstart_setlocale()
        return

    profile = args[0]
    locale = args[1]

    # always use UTC
    utc = True

    self.client.kickstart.profile.system.setLocale(self.session,
                                                   profile,
                                                   locale,
                                                   utc)

####################


def help_kickstart_setselinux(self):
    print('kickstart_setselinux: Set the SELinux mode for a Kickstart ' +
          'profile')
    print('usage: kickstart_setselinux PROFILE MODE')


def complete_kickstart_setselinux(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) == 3:
        modes = ['enforcing', 'permissive', 'disabled']
        return tab_completer(modes, text)


def do_kickstart_setselinux(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_kickstart_setselinux()
        return

    profile = args[0]
    mode = args[1]

    self.client.kickstart.profile.system.setSELinux(self.session,
                                                    profile,
                                                    mode)

####################


def help_kickstart_setpartitions(self):
    print('kickstart_setpartitions: Set the partitioning scheme for a ' +
          'Kickstart profile')
    print('usage: kickstart_setpartitions PROFILE')


def complete_kickstart_setpartitions(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_setpartitions(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_setpartitions()
        return

    profile = args[0]

    try:
        # get the current scheme so the user can edit it
        current = \
            self.client.kickstart.profile.system.getPartitioningScheme(
                self.session, profile)

        template = '\n'.join(current)
    except xmlrpclib.Fault:
        template = ''

    (partitions, _ignore) = editor(template=template, delete=True)

    print(partitions)
    if not self.user_confirm():
        return

    lines = partitions.split('\n')

    self.client.kickstart.profile.system.setPartitioningScheme(self.session,
                                                               profile,
                                                               lines)

####################


def help_kickstart_setdistribution(self):
    print('kickstart_setdistribution: Set the distribution for a ' +
          'Kickstart profile')
    print('usage: kickstart_setdistribution PROFILE DISTRIBUTION')


def complete_kickstart_setdistribution(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) == 3:
        return tab_completer(self.do_distribution_list('', True), text)


def do_kickstart_setdistribution(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_kickstart_setdistribution()
        return

    profile = args[0]
    distribution = args[1]

    self.client.kickstart.profile.setKickstartTree(self.session,
                                                   profile,
                                                   distribution)

####################


def help_kickstart_enablelogging(self):
    print('kickstart_enablelogging: Enable logging for a Kickstart profile')
    print('usage: kickstart_enablelogging PROFILE')


def complete_kickstart_enablelogging(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_enablelogging(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_enablelogging()
        return

    profile = args[0]

    self.client.kickstart.profile.setLogging(self.session,
                                             profile,
                                             True,
                                             True)

####################


def help_kickstart_addvariable(self):
    print('kickstart_addvariable: Add a variable to a Kickstart profile')
    print('usage: kickstart_addvariable PROFILE KEY VALUE')


def complete_kickstart_addvariable(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_addvariable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_kickstart_addvariable()
        return

    profile = args[0]
    key = args[1]
    value = ' '.join(args[2:])

    variables = self.client.kickstart.profile.getVariables(self.session,
                                                           profile)

    variables[key] = value

    self.client.kickstart.profile.setVariables(self.session,
                                               profile,
                                               variables)

####################


def help_kickstart_updatevariable(self):
    print('kickstart_updatevariable: Update a variable in a Kickstart ' +
          'profile')
    print('usage: kickstart_updatevariable PROFILE KEY VALUE')


def complete_kickstart_updatevariable(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        variables = {}
        try:
            variables = \
                self.client.kickstart.profile.getVariables(self.session,
                                                           parts[1])
        except xmlrpclib.Fault:
            pass

        return tab_completer(variables.keys(), text)


def do_kickstart_updatevariable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_kickstart_updatevariable()
        return

    return self.do_kickstart_addvariable(' '.join(args))

####################


def help_kickstart_removevariables(self):
    print('kickstart_removevariables: Remove variables from a ' +
          'Kickstart profile')
    print('usage: kickstart_removevariables PROFILE <KEY ...>')


def complete_kickstart_removevariables(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        variables = {}
        try:
            variables = \
                self.client.kickstart.profile.getVariables(self.session,
                                                           parts[1])
        except xmlrpclib.Fault:
            pass

        return tab_completer(variables.keys(), text)


def do_kickstart_removevariables(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removevariables()
        return

    profile = args[0]
    keys = args[1:]

    variables = self.client.kickstart.profile.getVariables(self.session,
                                                           profile)

    for key in keys:
        if key in variables:
            del variables[key]

    self.client.kickstart.profile.setVariables(self.session,
                                               profile,
                                               variables)

####################


def help_kickstart_listvariables(self):
    print('kickstart_listvariables: List the variables of a Kickstart ' +
          'profile')
    print('usage: kickstart_listvariables PROFILE')


def complete_kickstart_listvariables(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listvariables(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listvariables()
        return

    profile = args[0]

    variables = self.client.kickstart.profile.getVariables(self.session,
                                                           profile)

    for v in variables:
        print('%s = %s' % (v, variables[v]))

####################


def help_kickstart_addoption(self):
    print('kickstart_addoption: Set an option for a Kickstart profile')
    print('usage: kickstart_addoption PROFILE KEY [VALUE]')


def complete_kickstart_addoption(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) == 3:
        return tab_completer(sorted(self.KICKSTART_OPTIONS), text)


def do_kickstart_addoption(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_addoption()
        return

    profile = args[0]
    key = args[1]

    if len(args) > 2:
        value = ' '.join(args[2:])
    else:
        value = ''

    # only pre-defined options can be set as 'advanced options'
    if key in self.KICKSTART_OPTIONS:
        advanced = \
            self.client.kickstart.profile.getAdvancedOptions(self.session,
                                                             profile)

        # remove any instances of this key from the current list
        for item in advanced:
            if item.get('name') == key:
                advanced.remove(item)
                break

        advanced.append({'name': key, 'arguments': value})

        self.client.kickstart.profile.setAdvancedOptions(self.session,
                                                         profile,
                                                         advanced)
    else:
        logging.warning('%s needs to be set as a custom option' % key)
        return

####################


def help_kickstart_removeoptions(self):
    print('kickstart_removeoptions: Remove options from a Kickstart profile')
    print('usage: kickstart_removeoptions PROFILE <OPTION ...>')


def complete_kickstart_removeoptions(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        try:
            options = self.client.kickstart.profile.getAdvancedOptions(
                self.session, parts[1])

            options = [o.get('name') for o in options]
        except xmlrpclib.Fault:
            options = self.KICKSTART_OPTIONS

        return tab_completer(sorted(options), text)


def do_kickstart_removeoptions(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removeoptions()
        return

    profile = args[0]
    keys = args[1:]

    advanced = \
        self.client.kickstart.profile.getAdvancedOptions(self.session,
                                                         profile)

    # remove any instances of this key from the current list
    for key in keys:
        for item in advanced:
            if item.get('name') == key:
                advanced.remove(item)

    self.client.kickstart.profile.setAdvancedOptions(self.session,
                                                     profile,
                                                     advanced)

####################


def help_kickstart_listoptions(self):
    print('kickstart_listoptions: List the options of a Kickstart ' +
          'profile')
    print('usage: kickstart_listoptions PROFILE')


def complete_kickstart_listoptions(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listoptions(self, args):
    arg_parser = get_argument_parser()

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listoptions()
        return

    profile = args[0]

    options = self.client.kickstart.profile.getAdvancedOptions(self.session,
                                                               profile)

    for o in sorted(options, key=itemgetter('name')):
        if o.get('arguments'):
            print('%s %s' % (o.get('name'), o.get('arguments')))

####################


def help_kickstart_listcustomoptions(self):
    print('kickstart_listcustomoptions: List the custom options of a ' +
          'Kickstart profile')
    print('usage: kickstart_listcustomoptions PROFILE')


def complete_kickstart_listcustomoptions(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listcustomoptions(self, args):
    arg_parser = get_argument_parser()

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listcustomoptions()
        return

    profile = args[0]

    options = self.client.kickstart.profile.getCustomOptions(self.session,
                                                             profile)

    for o in options:
        if 'arguments' in o:
            print(o.get('arguments'))

####################


def help_kickstart_setcustomoptions(self):
    print('kickstart_setcustomoptions: Set custom options for a ' +
          'Kickstart profile')
    print('usage: kickstart_setcustomoptions PROFILE')


def complete_kickstart_setcustomoptions(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_setcustomoptions(self, args):
    arg_parser = get_argument_parser()

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_setcustomoptions()
        return

    profile = args[0]

    options = self.client.kickstart.profile.getCustomOptions(self.session,
                                                             profile)

    # the first item in the list is missing the 'arguments' key
    old_options = []
    for o in options:
        if 'arguments' in o:
            old_options.append(o.get('arguments'))

    old_options = '\n'.join(old_options)

    # let the user edit the custom options
    (new_options, _ignore) = editor(template=old_options,
                                    delete=True)

    new_options = new_options.split('\n')

    self.client.kickstart.profile.setCustomOptions(self.session,
                                                   profile,
                                                   new_options)

####################


def help_kickstart_addchildchannels(self):
    print('kickstart_addchildchannels: Add a child channels to a ' +
          'Kickstart profile')
    print('usage: kickstart_addchildchannels PROFILE <CHANNEL ...>')


def complete_kickstart_addchildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        profile = parts[1]

        try:
            tree = \
                self.client.kickstart.profile.getKickstartTree(self.session,
                                                               profile)

            tree_details = self.client.kickstart.tree.getDetails(
                self.session, tree)

            base_channel = \
                self.client.channel.software.getDetails(self.session,
                                                        tree_details.get('channel_id'))

            parent_channel = base_channel.get('label')
        except xmlrpclib.Fault:
            return []

        return tab_completer(self.list_child_channels(
            parent=parent_channel), text)


def do_kickstart_addchildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_addchildchannels()
        return

    profile = args[0]
    new_channels = args[1:]

    channels = self.client.kickstart.profile.getChildChannels(self.session,
                                                              profile)

    channels.extend(new_channels)

    self.client.kickstart.profile.setChildChannels(self.session,
                                                   profile,
                                                   channels)

####################


def help_kickstart_removechildchannels(self):
    print('kickstart_removechildchannels: Remove child channels from ' +
          'a Kickstart profile')
    print('usage: kickstart_removechildchannels PROFILE <CHANNEL ...>')


def complete_kickstart_removechildchannels(self, text, line, beg,
                                           end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_kickstart_listchildchannels(
            parts[1], True), text)


def do_kickstart_removechildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removechildchannels()
        return

    profile = args[0]
    to_remove = args[1:]

    channels = self.client.kickstart.profile.getChildChannels(self.session,
                                                              profile)

    for channel in to_remove:
        if channel in channels:
            channels.remove(channel)

    self.client.kickstart.profile.setChildChannels(self.session,
                                                   profile,
                                                   channels)

####################


def help_kickstart_listchildchannels(self):
    print('kickstart_listchildchannels: List the child channels of a ' +
          'Kickstart profile')
    print('usage: kickstart_listchildchannels PROFILE')


def complete_kickstart_listchildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listchildchannels(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listchildchannels()
        return

    profile = args[0]

    channels = self.client.kickstart.profile.getChildChannels(self.session,
                                                              profile)

    if doreturn:
        return channels
    else:
        if channels:
            print('\n'.join(sorted(channels)))

####################


def help_kickstart_addfilepreservations(self):
    print('kickstart_addfilepreservations: Add file preservations to a ' +
          'Kickstart profile')
    print('usage: kickstart_addfilepreservations PROFILE <FILELIST ...>')


def complete_kickstart_addfilepreservations(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) == 3:
        return tab_completer(self.do_filepreservation_list('', True),
                             text)


def do_kickstart_addfilepreservations(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_addfilepreservations()
        return

    profile = args[0]
    files = args[1:]

    self.client.kickstart.profile.system.addFilePreservations(self.session,
                                                              profile,
                                                              files)

####################


def help_kickstart_removefilepreservations(self):
    print('kickstart_removefilepreservations: Remove file ' +
          'preservations from a Kickstart profile')
    print('usage: kickstart_removefilepreservations PROFILE <FILE ...>')


def complete_kickstart_removefilepreservations(self, text, line, beg,
                                               end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        files = []

        try:
            # only tab complete files currently assigned to the profile
            files = \
                self.client.kickstart.profile.system.listFilePreservations(
                    self.session, parts[1])
            files = [f.get('name') for f in files]
        except xmlrpclib.Fault:
            return []

        return tab_completer(files, text)


def do_kickstart_removefilepreservations(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removefilepreservations()
        return

    profile = args[0]
    files = args[1:]

    self.client.kickstart.profile.system.removeFilePreservations(
        self.session, profile, files)

####################


def help_kickstart_listpackages(self):
    print('kickstart_listpackages: List the packages for a Kickstart ' +
          'profile')
    print('usage: kickstart_listpackages PROFILE')


def complete_kickstart_listpackages(self, text, line, beg, end):
    return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listpackages(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listpackages()
        return

    profile = args[0]

    packages = \
        self.client.kickstart.profile.software.getSoftwareList(self.session,
                                                               profile)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(packages))

####################


def help_kickstart_addpackages(self):
    print('kickstart_addpackages: Add packages to a Kickstart profile')
    print('usage: kickstart_addpackages PROFILE <PACKAGE ...>')


def complete_kickstart_addpackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(), text)


def do_kickstart_addpackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) >= 2:
        self.help_kickstart_addpackages()
        return

    profile = args[0]
    packages = args[1:]

    self.client.kickstart.profile.software.appendToSoftwareList(
        self.session, profile, packages)

####################


def help_kickstart_removepackages(self):
    print('kickstart_removepackages: Remove packages from a Kickstart ' +
          'profile')
    print('usage: kickstart_removepackages PROFILE <PACKAGE ...>')


def complete_kickstart_removepackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True),
                             text)
    elif len(parts) > 2:
        return tab_completer(self.do_kickstart_listpackages(
            parts[1], True), text)


def do_kickstart_removepackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_kickstart_removepackages()
        return

    profile = args[0]
    to_remove = args[1:]

    # setSoftwareList requires a new list of packages, so grab
    # the old list and remove the list of packages from the user
    packages = self.do_kickstart_listpackages(profile, True)
    for package in to_remove:
        if package in packages:
            packages.remove(package)

    self.client.kickstart.profile.software.setSoftwareList(self.session,
                                                           profile,
                                                           packages)

####################


def help_kickstart_listscripts(self):
    print('kickstart_listscripts: List the scripts for a Kickstart ' +
          'profile')
    print('usage: kickstart_listscripts PROFILE')


def complete_kickstart_listscripts(self, text, line, beg, end):
    return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_listscripts(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_listscripts()
        return

    profile = args[0]

    scripts = \
        self.client.kickstart.profile.listScripts(self.session, profile)

    add_separator = False

    for script in scripts:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('ID:          %i' % script.get('id'))
        print('Type:        %s' % script.get('script_type'))
        print('Chroot:      %s' % script.get('chroot'))
        print('Interpreter: %s' % script.get('interpreter'))
        print('')
        print('Contents')
        print('--------')
        print(script.get('contents'))

####################


def help_kickstart_addscript(self):
    print('kickstart_addscript: Add a script to a Kickstart profile')
    print('''usage: kickstart_addscript PROFILE [options])

options:
  -p PROFILE
  -e EXECUTION_TIME ['pre', 'post']
  -i INTERPRETER
  -f FILE
  -c execute in a chroot environment
  -t ENABLING_TEMPLATING''')


def complete_kickstart_addscript(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_addscript(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-p', '--profile')
    arg_parser.add_argument('-e', '--execution-time')
    arg_parser.add_argument('-c', '--chroot', action='store_true')
    arg_parser.add_argument('-t', '--template', action='store_true')
    arg_parser.add_argument('-i', '--interpreter')
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        if args:
            options.profile = args[0]
        else:
            options.profile = prompt_user('Profile Name:', noblank=True)

        options.execution_time = prompt_user('Pre/Post Script [post]:')
        options.chroot = prompt_user('Chrooted [Y/n]:')
        options.interpreter = prompt_user('Interpreter [/bin/bash]:')

        # get the contents of the script
        if self.user_confirm('Read an existing file [y/N]:',
                             nospacer=True, ignore_yes=True):
            options.file = prompt_user('File:')
        else:
            (options.contents, _ignore) = editor(delete=True)

        # check user input
        if options.interpreter == '':
            options.interpreter = '/bin/bash'

        if re.match('n', options.chroot, re.I):
            options.chroot = False
        else:
            options.chroot = True

        if re.match('n', options.template, re.I):
            options.template = False
        else:
            options.template = True

        if re.match('pre', options.execution_time, re.I):
            options.execution_time = 'pre'
        else:
            options.execution_time = 'post'
    else:
        if not options.profile:
            logging.error('The Kickstart name is required')
            return

        if not options.file:
            logging.error('A filename is required')
            return

        if not options.execution_time:
            logging.error('The execution time is required')
            return

        if not options.chroot:
            options.chroot = False

        if not options.interpreter:
            options.interpreter = '/bin/bash'

        if not options.template:
            options.template = False

    if options.file:
        options.contents = read_file(options.file)

    print('')
    print('Profile Name:   %s' % options.profile)
    print('Execution Time: %s' % options.execution_time)
    print('Chroot:         %s' % options.chroot)
    print('Template:       %s' % options.template)
    print('Interpreter:    %s' % options.interpreter)
    print('Contents:')
    print(options.contents)

    if not self.user_confirm():
        return

    self.client.kickstart.profile.addScript(self.session,
                                            options.profile,
                                            options.contents,
                                            options.interpreter,
                                            options.execution_time,
                                            options.chroot,
                                            options.template)

####################


def help_kickstart_removescript(self):
    print('kickstart_removescript: Remove a script from a Kickstart profile')
    print('usage: kickstart_removescript PROFILE [ID]')


def complete_kickstart_removescript(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_removescript(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_kickstart_removescript()
        return

    profile = args[0]

    script_id = 0

    # allow a script ID to be passed in
    if len(args) == 2:
        try:
            script_id = int(args[1])
        except ValueError:
            logging.error('Invalid script ID')

    # print(the scripts for the user to review)
    self.do_kickstart_listscripts(profile)

    if not script_id:
        while script_id == 0:
            print('')
            userinput = prompt_user('Script ID:', noblank=True)

            try:
                script_id = int(userinput)
            except ValueError:
                logging.error('Invalid script ID')

    if not self.user_confirm('Remove this script [y/N]:'):
        return

    self.client.kickstart.profile.removeScript(self.session,
                                               profile,
                                               script_id)

####################


def help_kickstart_clone(self):
    print('kickstart_clone: Clone a Kickstart profile')
    print('''usage: kickstart_clone [options])

options:
  -n NAME
  -c CLONE_NAME''')


def complete_kickstart_clone(self, text, line, beg, end):
    return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_clone(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-c', '--clonename')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        profiles = self.do_kickstart_list('', True)
        print('')
        print('Kickstart Profiles')
        print('------------------')
        print('\n'.join(sorted(profiles)))
        print('')

        options.name = prompt_user('Original Profile:', noblank=True)

        options.clonename = prompt_user('Cloned Profile:', noblank=True)
    else:

        if not options.name:
            logging.error('The Kickstart name is required')
            return

        if not options.clonename:
            logging.error('The Kickstart clone name is required')
            return

    self.client.kickstart.cloneProfile(self.session,
                                       options.name,
                                       options.clonename)

####################


def help_kickstart_export(self):
    print('kickstart_export: export kickstart profile(s) to json format file')
    print('''usage: kickstart_export <KSPROFILE>... [options])
options:
    -f outfile.json : specify an output filename, defaults to <KSPROFILE>.json
                      if exporting a single kickstart, profiles.json for multiple
                      kickstarts, or ks_all.json if no KSPROFILE specified
                      e.g (export ALL)

Note : KSPROFILE list is optional, default is to export ALL''')


def complete_kickstart_export(self, text, line, beg, end):
    return tab_completer(self.do_kickstart_list('', True), text)


def export_kickstart_getdetails(self, profile, kickstarts):

    # Get the initial ks details struct from the kickstarts list-of-struct,
    # which is returned from kickstart.listKickstarts()
    logging.debug("Getting kickstart profile details for %s" % profile)
    details = None
    for k in kickstarts:
        if k.get('label') == profile:
            details = k
            break
    logging.debug("Got basic details for %s : %s" % (profile, details))

    # Now use the various other API functions to build up a more complete
    # details struct for export.  Note there are a some ommisions from the API
    # e.g the "template" option which enables cobbler templating on scripts
    details['child_channels'] = \
        self.client.kickstart.profile.getChildChannels(self.session, profile)
    details['advanced_opts'] = \
        self.client.kickstart.profile.getAdvancedOptions(self.session, profile)
    details['software_list'] = \
        self.client.kickstart.profile.software.getSoftwareList(self.session,
                                                               profile)
    details['custom_opts'] = \
        self.client.kickstart.profile.getCustomOptions(self.session, profile)
    details['script_list'] = \
        self.client.kickstart.profile.listScripts(self.session, profile)
    details['ip_ranges'] = \
        self.client.kickstart.profile.listIpRanges(self.session, profile)
    logging.debug("About to get variable_list for %s" % profile)
    details['variable_list'] = \
        self.client.kickstart.profile.getVariables(self.session, profile)
    logging.debug("done variable_list for %s = %s" % (profile,
                                                      details['variable_list']))
    # just export the key names, then look for one with the same name on import
    details['activation_keys'] = [k['key'] for k in
                                  self.client.kickstart.profile.keys.getActivationKeys(self.session,
                                                                                       profile)]
    details['partitioning_scheme'] = \
        self.client.kickstart.profile.system.getPartitioningScheme(
            self.session, profile)
    if self.check_api_version('10.11'):
        details['reg_type'] = \
            self.client.kickstart.profile.system.getRegistrationType(self.session,
                                                                     profile)
    else:
        details['reg_type'] = "none"
    details['config_mgmt'] = \
        self.client.kickstart.profile.system.checkConfigManagement(
            self.session, profile)
    details['remote_cmds'] = \
        self.client.kickstart.profile.system.checkRemoteCommands(
            self.session, profile)
    # Just export the file preservation list names, then look for one with the
    # same name on import
    details['file_preservations'] = [
        f['name'] for f in self.client.kickstart.profile.system.listFilePreservations(
            self.session, profile)]
    # just export the key description/names , then look for one with the same
    # name on import
    details['gpg_ssl_keys'] = [k['description'] for k in
                               self.client.kickstart.profile.system.listKeys(self.session, profile)]

    # There's a setLogging() but no getLogging(), so we look in the rendered
    # kickstart to figure out if pre/post logging is enabled
    kscontents = self.kickstart_getcontents(profile)
    if re.search("pre --logfile", kscontents):
        logging.debug("Detected pre script logging")
        details['pre_logging'] = True
    else:
        details['pre_logging'] = False
    if re.search("post --logfile", kscontents):
        logging.debug("Detected post script logging")
        details['post_logging'] = True
    else:
        details['post_logging'] = False

    # There's also no way to get the "Kernel Options" and "Post Kernel Options"
    # The Post options can be derived from the grubby --default-kernel` --args
    # line in the kickstart, ugly but at least we can then show some of what's
    # missing in the warnings that get printed on import
    if re.search("`/sbin/grubby --default-kernel` --args=", kscontents):
        post_kopts = \
            kscontents.split("`/sbin/grubby --default-kernel` --args=")[1].\
            split("\"")[1]
        logging.debug("Post kernel options %s detected" % post_kopts)
        details['post_kopts'] = post_kopts

    # and now sort all the lists
    for i in details.keys():
        if isinstance(details[i], list):
            details[i].sort()

    return details


def do_kickstart_export(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    filename = ""
    if options.file != None:
        logging.debug("Passed filename do_kickstart_export %s" %
                      options.file)
        filename = options.file

    # Get the list of profiles to export and sort out the filename if required
    profiles = []
    if not args:
        if not filename:
            filename = "ks_all.json"
        logging.info("Exporting ALL kickstart profiles to %s" % filename)
        profiles = self.do_kickstart_list('', True)
    else:
        # allow globbing of kickstart kickstart names
        profiles = filter_results(self.do_kickstart_list('', True), args)
        logging.debug("kickstart_export called with args %s, profiles=%s" %
                      (args, profiles))
        if not profiles:
            logging.error("Error, no valid kickstart profile passed, " +
                          "check name is  correct with spacecmd kickstart_list")
            return
        if not filename:
            # No filename arg, so we try to do something sensible:
            # If we are exporting exactly one ks, we default to ksname.json
            # otherwise, generic ks_profiles.json name
            if len(profiles) == 1:
                filename = "%s.json" % profiles[0]
            else:
                filename = "ks_profiles.json"

    # First grab the list of basic details about all kickstarts because you
    # can't get details-per-label, call here to avoid potential duplicate calls
    # in export_kickstart_getdetails for multi-profile exports
    kickstarts = self.client.kickstart.listKickstarts(self.session)

    # Dump as a list of dict
    ksdetails_list = []
    for p in profiles:
        logging.info("Exporting ks %s to %s" % (p, filename))
        ksdetails_list.append(self.export_kickstart_getdetails(p, kickstarts))

    logging.debug("About to dump %d ks profiles to %s" %
                  (len(ksdetails_list), filename))
    # Check if filepath exists, if an existing file we prompt for confirmation
    if os.path.isfile(filename):
        if not self.user_confirm("File %s exists, " % filename +
                                 "confirm overwrite file? (y/n)"):
            return
    if json_dump_to_file(ksdetails_list, filename) != True:
        logging.error("Error saving exported kickstart profiles to file" %
                      filename)
        return

####################


def help_kickstart_importjson(self):
    print('kickstart_import: import kickstart profile(s) from json file')
    print('''usage: kickstart_import <JSONFILES...>''')


def do_kickstart_importjson(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        logging.error("Error, no filename passed")
        self.help_kickstart_import()
        return

    for filename in args:
        logging.debug("Passed filename do_kickstart_import %s" % filename)
        ksdetails_list = json_read_from_file(filename)
        if not ksdetails_list:
            logging.error("Error, could not read json data from %s" % filename)
            return
        for ksdetails in ksdetails_list:
            if self.import_kickstart_fromdetails(ksdetails) != True:
                logging.error("Error importing kickstart %s" %
                              ksdetails['name'])

# create a new ks based on the dict from export_kickstart_getdetails


def import_kickstart_fromdetails(self, ksdetails):

    # First we check that an existing kickstart with the same name does not exist
    existing_profiles = self.do_kickstart_list('', True)
    if ksdetails['name'] in existing_profiles:
        logging.error("ERROR : kickstart profile %s already exists! Skipping!"
                      % ksdetails['name'])
        return False
    # create the ks, we need to drop the org prefix from the ks name
    logging.info("Found ks %s" % ksdetails['name'])

    # Create the kickstart
    # for adding new profiles, we require a root password.
    # This is overridden when we set the 'advanced options'
    tmppw = 'foobar'
    virt_type = 'none'  # assume none as there's no API call to read this info
    ks_host = ''
    self.client.kickstart.createProfile(self.session, ksdetails['label'],
                                        virt_type, ksdetails['tree_label'], ks_host, tmppw)
    # Now set other options
    self.client.kickstart.profile.setChildChannels(
        self.session, ksdetails['label'], ksdetails['child_channels'])
    self.client.kickstart.profile.setAdvancedOptions(
        self.session, ksdetails['label'], ksdetails['advanced_opts'])
    self.client.kickstart.profile.system.setPartitioningScheme(
        self.session, ksdetails['label'], ksdetails['partitioning_scheme'])
    self.client.kickstart.profile.software.setSoftwareList(
        self.session, ksdetails['label'], ksdetails['software_list'])
    self.client.kickstart.profile.setCustomOptions(
        self.session, ksdetails['label'], [o and o.get('arguments') or "\n" for o in ksdetails['custom_opts']])
    self.client.kickstart.profile.setVariables(
        self.session, ksdetails['label'], ksdetails['variable_list'])
    self.client.kickstart.profile.system.setRegistrationType(
        self.session, ksdetails['label'], ksdetails['reg_type'])
    if ksdetails['config_mgmt']:
        self.client.kickstart.profile.system.enableConfigManagement(
            self.session, ksdetails['label'])
    if ksdetails['remote_cmds']:
        self.client.kickstart.profile.system.enableRemoteCommands(
            self.session, ksdetails['label'])
    # Add the scripts
    for script in ksdetails['script_list']:
        # Somewhere between spacewalk-java-1.2.39-85 and 1.2.39-108,
        # two new versions of listScripts and addScripts were added, which
        # allows us to correctly set the "template" checkbox on import
        # However, we can't detect this capability via API version since
        # the API version number is the same (10.11)
        # So, we look for the template key in the script dict and use the "new"
        # API call if we find it.  This will obviously break if migrating
        # kickstarts from a server with the new API call to one without it,
        # so ensure the target satellite is at least as up-to-date as the
        # satellite where the export was performed.
        if 'template' in script:
            ret = self.client.kickstart.profile.addScript(self.session,
                                                          ksdetails['label'], script['name'], script['contents'],
                                                          script['interpreter'], script[
                                                              'script_type'], script['chroot'],
                                                          script['template'])
        else:
            ret = self.client.kickstart.profile.addScript(
                self.session, ksdetails['label'], script['name'], script['contents'],
                script['interpreter'], script['script_type'], script['chroot'])
        if ret:
            logging.debug("Added %s script to profile" % script['script_type'])
        else:
            logging.error("Error adding %s script" % script['script_type'])
    # Specify ip ranges
    for iprange in ksdetails['ip_ranges']:
        if self.client.kickstart.profile.addIpRange(self.session,
                                                    ksdetails['label'], iprange['min'], iprange['max']):
            logging.debug("added ip range %s-%s" %
                          iprange['min'], iprange['max'])
        else:
            logging.warning("failed to add ip range %s-%s, continuing" %
                            iprange['min'], iprange['max'])
            continue
    # File preservations, only if the list exists
    existing_file_preservations = [
        x['name'] for x in self.client.kickstart.filepreservation.listAllFilePreservations(
            self.session)]
    if ksdetails['file_preservations']:
        for fp in ksdetails['file_preservations']:
            if fp in existing_file_preservations:
                if self.client.kickstart.profile.system.addFilePreservations(
                        self.session, ksdetails['label'], [fp]):
                    logging.debug("added file preservation '%s'" % fp)
                else:
                    logging.warning("failed to add file preservation %s, skipping" % fp)
            else:
                logging.warning("file preservation list %s doesn't exist, skipping" % fp)

    # Now add activationkeys, only if they exist
    existing_act_keys = [k['key'] for k in
                         self.client.activationkey.listActivationKeys(self.session)]
    for akey in ksdetails['activation_keys']:
        if akey in existing_act_keys:
            logging.debug("Adding activation key %s to profile" % akey)
            self.client.kickstart.profile.keys.addActivationKey(self.session,
                                                                ksdetails['label'], akey)
        else:
            logging.warning("Actvationkey %s does not exist on the " % akey +
                            "satellite, skipping")

    # The GPG/SSL keys, only if they exist
    existing_gpg_ssl_keys = [x['description'] for x in self.client.kickstart.keys.listAllKeys(self.session)]
    for key in ksdetails['gpg_ssl_keys']:
        if key in existing_gpg_ssl_keys:
            logging.debug("Adding GPG/SSL key %s to profile" % key)
            self.client.kickstart.profile.system.addKeys(self.session,
                                                         ksdetails['label'], [key])
        else:
            logging.warning("GPG/SSL key %s does not exist on the " % key +
                            "satellite, skipping")

    # The pre/post logging settings
    self.client.kickstart.profile.setLogging(self.session, ksdetails['label'],
                                             ksdetails['pre_logging'], ksdetails['post_logging'])

    # There are some frustrating ommisions from the API which means we can't
    # export/import some settings, so we post a warning that some manual
    # fixup may be required
    logging.warning("Due to API ommissions, there are some settings which" +
                    " cannot be imported, please check and fixup manually if necessary")
    logging.warning(" * Details->Preserve ks.cfg")
    logging.warning(" * Details->Comment")
    # Org default gets exported but no way to set it, so we can just show this
    # warning if they are trying to import an org_default profile
    if ksdetails['org_default']:
        logging.warning(" * Details->Organization Default Profile")
    # No way to set the kernel options
    logging.warning(" * Details->Kernel Options")
    # We can export Post kernel options (sort of, see above)
    # if they exist on import, flag a warning
    if 'post_kopts' in ksdetails:
        logging.warning(" * Details->Post Kernel Options : %s" %
                        ksdetails['post_kopts'])
    return True

####################
# kickstart helper


def is_kickstart(self, name):
    if not name:
        return
    return name in self.do_kickstart_list(name, True)


def check_kickstart(self, name):
    if not name:
        logging.error("no kickstart label given")
        return False
    if not self.is_kickstart(name):
        logging.error("invalid kickstart label " + name)
        return False
    return True


def dump_kickstart(self, name, replacedict=None, excludes=None):
    excludes = excludes or ["Org Default:"]
    content = self.do_kickstart_details(name)

    content = get_normalized_text(content, replacedict=replacedict, excludes=excludes)

    return content

####################


def help_kickstart_diff(self):
    print('kickstart_diff: diff kickstart files')
    print('')
    print('usage: kickstart_diff SOURCE_CHANNEL TARGET_CHANNEL')


def complete_kickstart_diff(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    if args == 3:
        return tab_completer(self.do_kickstart_list('', True), text)
    return []


def do_kickstart_diff(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_kickstart_diff()
        return

    source_channel = args[0]
    if not self.check_kickstart(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_kickstart_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_kickstart_getcorresponding(source_channel)
    if not self.check_kickstart(target_channel):
        return

    source_replacedict, target_replacedict = get_string_diff_dicts(source_channel, target_channel)

    source_data = self.dump_kickstart(source_channel, source_replacedict)
    target_data = self.dump_kickstart(target_channel, target_replacedict)

    return diff(source_data, target_data, source_channel, target_channel)

####################


def help_kickstart_getupdatetype(self):
    print('kickstart_getupdatetype: Get the update type for a kickstart profile(s)')
    print('usage: kickstart_getupdatetype PROFILE')
    print('usage: kickstart_getupdatetype PROFILE1 PROFILE2')
    print('usage: kickstart_getupdatetype \"PROF*\"')


def complete_kickstart_getupdatetype(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_getupdatetype(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 1:
        self.help_kickstart_getupdatetype()
        return

    # allow globbing of kickstart labels
    all_labels = self.do_kickstart_list('', True)
    labels = filter_results(all_labels, args)
    logging.debug("Got labels to list the update type %s" % labels)

    if not labels:
        logging.error("No valid kickstart labels passed as arguments!")
        self.help_kickstart_getupdatetype()
        return

    for label in labels:
        if not label in all_labels:
            logging.error("kickstart label %s doesn't exist!" % label)
            continue

        updatetype = self.client.kickstart.profile.getUpdateType(self.session, label)

        if len(labels) == 1:
            print(updatetype)
        elif len(labels) > 1:
            print(label, ":", updatetype)

####################


def help_kickstart_setupdatetype(self):
    print('kickstart_setupdatetype: Set the update type for a kickstart profile(s)')
    print('''usage: kickstart_setupdatetype [options] KS_LABEL)

options:
    -u UPDATE_TYPE ['red_hat', 'all', 'none']''')


def do_kickstart_setupdatetype(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-u', '--update-type')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):

        print('Update Types')
        print('--------------------')
        print('\n'.join(sorted(self.UPDATE_TYPES)))
        print('')

        options.update_type = prompt_user('Update Type [none]:')
        if options.update_type == '' or options.update_type not in self.UPDATE_TYPES:
            options.update_type = 'none'

    else:
        if not options.update_type:
            options.update_type = 'none'

    # allow globbing of kickstart labels
    all_labels = self.do_kickstart_list('', True)
    labels = filter_results(all_labels, args)
    logging.debug("Got labels to set the update type %s" % labels)

    if not labels:
        logging.error("No valid kickstart labels passed as arguments!")
        self.help_kickstart_setupdatetype()
        return

    for label in labels:
        if not label in all_labels:
            logging.error("kickstart label %s doesn't exist!" % label)
            continue

        self.client.kickstart.profile.setUpdateType(self.session, label, options.update_type)

####################


def help_kickstart_getsoftwaredetails(self):
    print('kickstart_getsoftwaredetails: Gets kickstart profile software details')
    print('usage: kickstart_getsoftwaredetails KS_LABEL')
    print('usage: kickstart_getsoftwaredetails KS_LABEL KS_LABEL2 ...')


def complete_kickstart_getsoftwaredetails(self, text, line, beg, end):
    if len(line.split(' ')) >= 2:
        return tab_completer(self.do_kickstart_list('', True), text)


def do_kickstart_getsoftwaredetails(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 1:
        self.help_kickstart_getsoftwaredetails()
        return

    # allow globbing of kickstart labels
    all_labels = self.do_kickstart_list('', True)
    labels = filter_results(all_labels, args)
    logging.debug("Got labels to set the update type %s" % labels)

    if not labels:
        logging.error("No valid kickstart labels passed as arguments!")
        self.help_kickstart_getsoftwaredetails()
        return

    for label in labels:
        if not label in all_labels:
            logging.error("kickstart label %s doesn't exist!" % label)
            continue

        software_details = self.client.kickstart.profile.software.getSoftwareDetails(self.session, label)

        if len(labels) == 1:
            print("noBase:        %s" % software_details.get("noBase"))
            print("ignoreMissing: %s" % software_details.get("ignoreMissing"))
        elif len(labels) > 1:
            print("Kickstart Label: %s" % label)
            print("noBase:          %s" % software_details.get("noBase"))
            print("ignoreMissing:   %s" % software_details.get("ignoreMissing"))
            print('')

####################


def help_kickstart_setsoftwaredetails(self):
    print('kickstart_setsoftwaredetails: Sets kickstart profile software details.')
    print('usage: kickstart_setsoftwaredetails PROFILE KICKSTART_PACKAGES_INFO VALUE')
    print('usage: kickstart_setsoftwaredetails PROFILE KICKSTART_PACKAGES_INFO VALUE KICKSTART_PACKAGES_INFO VALUE')

def complete_kickstart_setsoftwaredetails(self, text, line, beg, end):
    parts = line.split(' ')
    length = len(parts)

    if length == 2:
        return tab_completer(self.do_kickstart_list('', True), text)
    if length in [3, 5]:
        if 'noBase' in parts:
            return tab_completer(['ignoreMissing'], text)
        if 'ignoreMissing' in parts:
            return tab_completer(['noBase'], text)

        kspkginfo = ['noBase', 'ignoreMissing']
        return tab_completer(kspkginfo, text)
    if length in [4, 6]:
        mode= ['True', 'False']
        return tab_completer(mode, text)


def do_kickstart_setsoftwaredetails(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)
    length = len(args)
    kspkginfo = ['noBase', 'ignoreMissing']
    mode = ['True', 'False']

    if length < 1 or length not in [3, 5]:
        self.help_kickstart_setsoftwaredetails()
        return

    if args[0] not in self.do_kickstart_list('', True):
        print("Selected profile does not exist")
        return
    if args[1] not in kspkginfo or args[2] not in mode:
        print("Enter valid input")
        self.help_kickstart_setsoftwaredetails()
        return
    if length==5:
        if (args[3] not in kspkginfo or args[4] not in mode) or args[1] == args[3]:
            print("Enter valid input")
            self.help_kickstart_setsoftwaredetails()
            return

    args[2] = string_to_bool(args[2])
    if length == 5:
        args[4] = string_to_bool(args[4])

    profile = args[0]
    if length == 3:
        software_details = self.client.kickstart.profile.software.getSoftwareDetails(self.session, profile)

        if args[1] == 'noBase':
            kspkginfo= {'noBase': args[2], 'ignoreMissing': string_to_bool(software_details.get("ignoreMissing"))}
        elif args[1] == 'ignoreMissing':
            kspkginfo= {'noBase': string_to_bool(software_details.get("noBase")), 'ignoreMissing':args[2]}
    else:
        if args[3] == 'noBase':
            kspkginfo= {'noBase': args[4], 'ignoreMissing': args[2]}
        elif args[3] == 'ignoreMissing':
            kspkginfo= {'noBase': args[2], 'ignoreMissing':args[4]}

    self.client.kickstart.profile.software.setSoftwareDetails(self.session,
                                                              profile,
                                                              kspkginfo)
0707010000001F000081B40000000000000000000000015DA8415F000070C0000000000000000000000000000000000000001E00000000spacecmd/src/spacecmd/misc.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import logging
import readline
import shlex
from getpass import getpass
try: # python 3
    from configparser import NoOptionError
except ImportError: # python 2
    from ConfigParser import NoOptionError

from time import sleep
try: # python 3
    from xmlrpc import client as xmlrpclib
except ImportError: # python2
    import xmlrpclib
from spacecmd.utils import *

# list of system selection options for the help output
HELP_SYSTEM_OPTS = '''<SYSTEMS> can be any of the following:
name
ssm (see 'help ssm')
search:QUERY (see 'help system_search')
group:GROUP
channel:CHANNEL
'''

HELP_TIME_OPTS = '''Dates can be any of the following:
Explicit Dates:
Dates can be expressed as explicit date strings in the YYYYMMDD[HHMMSS]
format.  The year, month and day are required, while the hours and
minutes are not; the hours and minutes will default to 0000 if no
values are provided.

Deltas:
Dates can be expressed as delta values.  For example, '2h' would
mean 2 hours in the future.  You can also use negative values to
express times in the past (e.g., -7d would be one week ago).

Units:
s -> seconds
m -> minutes
h -> hours
d -> days
'''

####################

# life of caches in seconds
SYSTEM_CACHE_TTL = 3600
PACKAGE_CACHE_TTL = 86400
ERRATA_CACHE_TTL = 86400

MINIMUM_API_VERSION = 10.8

SEPARATOR = '\n' + '#' * 30 + '\n'

####################

ENTITLEMENTS = ['enterprise_entitled',
                'virtualization_host'
               ]

SYSTEM_SEARCH_FIELDS = ['id', 'name', 'ip', 'hostname',
                        'device', 'vendor', 'driver', 'uuid']

CONTACT_METHODS = ['default', 'ssh-push', 'ssh-push-tunnel']

####################


def help_systems(self):
    print(HELP_SYSTEM_OPTS)


def help_time(self):
    print(HELP_TIME_OPTS)

####################


def help_clear(self):
    print('clear: clear the screen')
    print('usage: clear')


def do_clear(self, args):
    os.system('clear')

####################


def help_clear_caches(self):
    print('clear_caches: Clear the internal caches kept for systems and packages')
    print('usage: clear_caches')


def do_clear_caches(self, args):
    self.clear_system_cache()
    self.clear_package_cache()
    self.clear_errata_cache()

####################


def help_get_apiversion(self):
    print('get_apiversion: Display the API version of the server')
    print('usage: get_apiversion')


def do_get_apiversion(self, args):
    print(self.client.api.getVersion())

####################


def help_get_serverversion(self):
    print('get_serverversion: Display the version of the server')
    print('usage: get_serverversion')


def do_get_serverversion(self, args):
    print(self.client.api.systemVersion())


####################


def help_list_proxies(self):
    print('list_proxies: List the proxies within the user\'s organization ')
    print('usage: list_proxies')


def do_list_proxies(self, args):
    proxies = self.client.satellite.listProxies(self.session)
    print(proxies)

####################


def help_get_session(self):
    print('get_session: Show the current session string')
    print('usage: get_session')


def do_get_session(self, args):
    if self.session:
        print(self.session)
    else:
        logging.error('No session found')

####################


def help_help(self):
    print('help: Show help for the given command')
    print('usage: help COMMAND')

####################


def help_history(self):
    print('history: List your command history')
    print('usage: history')


def do_history(self, args):
    for i in range(1, readline.get_current_history_length()):
        print('%s  %s' % (str(i).rjust(4), readline.get_history_item(i)))

####################


def help_toggle_confirmations(self):
    print('toggle_confirmations: Toggle confirmation messages on/off')
    print('usage: toggle_confirmations')


def do_toggle_confirmations(self, args):
    if self.options.yes:
        self.options.yes = False
        print('Confirmation messages are enabled')
    else:
        self.options.yes = True
        logging.warning('Confirmation messages are DISABLED!')

####################


def help_login(self):
    print('login: Connect to a Spacewalk server')
    print('usage: login [USERNAME] [SERVER]')


def do_login(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    # logout before logging in again
    if self.session:
        logging.warning('You are already logged in')
        return True

    # an argument passed to the function get precedence
    if len(args) == 2:
        server = args[1]
    else:
        # use the server we were already using
        server = self.config['server']

    # bail out if not server was given
    if not server:
        logging.warning('No server specified')
        return False

    # load the server-specific configuration
    self.load_config_section(server)

    # an argument passed to the function get precedence
    if args:
        username = args[0]
    elif 'username' in self.config:
        # use the username from before
        username = self.config['username']
    elif self.options.username:
        # use the username from before
        username = self.options.username
    else:
        username = ''

    # set the protocol
    if 'nossl' in self.config and self.config['nossl']:
        proto = 'http'
    else:
        proto = 'https'

    server_url = '%s://%s/rpc/api' % (proto, server)

    # this will enable spewing out all client/server traffic
    verbose_xmlrpc = False
    if self.options.debug > 1:
        verbose_xmlrpc = True

    # connect to the server
    logging.debug('Connecting to %s', server_url)
    self.client = xmlrpclib.Server(server_url, verbose=verbose_xmlrpc)

    # check the API to verify connectivity
    # pylint: disable=W0702
    try:
        self.api_version = self.client.api.getVersion()
        logging.debug('Server API Version = %s', self.api_version)
    except:
        if self.options.debug > 0:
            e = sys.exc_info()[0]
            logging.exception(e)

        logging.error('Failed to connect to %s', server_url)
        self.client = None
        return False

    # ensure the server is recent enough
    if float(self.api_version) < self.MINIMUM_API_VERSION:
        logging.error('API (%s) is too old (>= %s required)',
                      self.api_version, self.MINIMUM_API_VERSION)

        self.client = None
        return False

    # store the session file in the server's own directory
    session_file = os.path.join(self.conf_dir, server, 'session')

    # retrieve a cached session
    if os.path.isfile(session_file) and not self.options.password:
        try:
            sessionfile = open(session_file, 'r')

            # read the session (format = username:session)
            for line in sessionfile:
                parts = line.split(':')

                # if a username was passed, make sure it matches
                if username:
                    if parts[0] == username:
                        self.session = parts[1]
                else:
                    # get the username from the cache if one
                    # wasn't passed by the user
                    username = parts[0]
                    self.session = parts[1]

            sessionfile.close()
        except IOError:
            logging.error('Could not read %s', session_file)

    # check the cached credentials by doing an API call
    if self.session:
        try:
            logging.debug('Using cached credentials from %s', session_file)

            self.client.user.listAssignableRoles(self.session)
        except xmlrpclib.Fault:
            logging.warning('Cached credentials are invalid')
            self.current_user = ''
            self.session = ''

    # attempt to login if we don't have a valid session yet
    if not self.session:
        if username:
            logging.info('Spacewalk Username: %s', username)
        else:
            username = prompt_user('Spacewalk Username:', noblank=True)

        if self.options.password:
            password = self.options.password

            # remove this from the options so that if 'login' is called
            # again, the user is prompted for the information
            self.options.password = None
        elif 'password' in self.config:
            password = self.config['password']
        else:
            password = getpass('Spacewalk Password: ')

        # login to the server
        try:
            self.session = self.client.auth.login(username, password)

            # don't keep the password around
            password = None
        except xmlrpclib.Fault:
            logging.error('Invalid credentials')
            return False

        try:
            # make sure ~/.spacecmd/<server> exists
            conf_dir = os.path.join(self.conf_dir, server)

            if not os.path.isdir(conf_dir):
                os.mkdir(conf_dir, int('0700', 8))

            # add the new cache to the file
            line = '%s:%s\n' % (username, self.session)

            # write the new cache file out
            sessionfile = open(session_file, 'w')
            sessionfile.write(line)
            sessionfile.close()
        except IOError:
            logging.error('Could not write session file')

    # load the system/package/errata caches
    self.load_caches(server, username)

    # keep track of who we are and who we're connected to
    self.current_user = username
    self.server = server

    logging.info('Connected to %s as %s', server_url, username)

    return True

####################


def help_logout(self):
    print('logout: Disconnect from the server')
    print('usage: logout')


def do_logout(self, args):
    if self.session:
        self.client.auth.logout(self.session)

    self.session = ''
    self.current_user = ''
    self.server = ''
    self.do_clear_caches('')

####################


def help_whoami(self):
    print('whoami: Print the name of the currently logged in user')
    print('usage: whoami')


def do_whoami(self, args):
    if self.current_user:
        print(self.current_user)
    else:
        logging.warning("You are not logged in")

####################


def help_whoamitalkingto(self):
    print('whoamitalkingto: Print the name of the server')
    print('usage: whoamitalkingto')


def do_whoamitalkingto(self, args):
    if self.server:
        print(self.server)
    else:
        logging.warning('Yourself')

####################


def tab_complete_errata(self, text):
    options = self.do_errata_list('', True)
    options.append('search:')

    return tab_completer(options, text)


def tab_complete_systems(self, text):
    if re.match('group:', text):
        # prepend 'group' to each item for tab completion
        groups = ['group:%s' % g for g in self.do_group_list('', True)]

        return tab_completer(groups, text)
    if re.match('channel:', text):
        # prepend 'channel' to each item for tab completion
        channels = ['channel:%s' % s
                    for s in self.do_softwarechannel_list('', True)]

        return tab_completer(channels, text)
    if re.match('search:', text):
        # prepend 'search' to each item for tab completion
        fields = ['search:%s:' % f for f in self.SYSTEM_SEARCH_FIELDS]
        return tab_completer(fields, text)

    options = self.get_system_names()

    # add our special search options
    options.extend(['group:', 'channel:', 'search:'])

    return tab_completer(options, text)


def remove_last_history_item(self):
    last = readline.get_current_history_length() - 1

    if last >= 0:
        readline.remove_history_item(last)


def clear_errata_cache(self):
    self.all_errata = {}
    self.errata_cache_expire = datetime.now()
    self.save_errata_cache()


def get_errata_names(self):
    return sorted([e.get('advisory_name') for e in self.all_errata])


def get_erratum_id(self, name):
    if name in self.all_errata:
        return self.all_errata[name]['id']


def get_erratum_name(self, erratum_id):
    for erratum in self.all_errata:
        if self.all_errata[erratum]['id'] == erratum_id:
            return erratum


def generate_errata_cache(self, force=False):
    if not force and datetime.now() < self.errata_cache_expire:
        return

    if not self.options.quiet:
        # tell the user what's going on
        self.replace_line_buffer('** Generating errata cache **')

    channels = self.client.channel.listSoftwareChannels(self.session)
    channels = [c.get('label') for c in channels]

    for c in channels:
        try:
            errata = \
                self.client.channel.software.listErrata(self.session, c)
        except xmlrpclib.Fault:
            logging.debug('No access to %s', c)
            continue

        for erratum in errata:
            if erratum.get('advisory_name') not in self.all_errata:
                self.all_errata[erratum.get('advisory_name')] = \
                    {'id': erratum.get('id'),
                     'advisory_name': erratum.get('advisory_name'),
                     'advisory_type': erratum.get('advisory_type'),
                     'date': erratum.get('date'),
                     'advisory_synopsis': erratum.get('advisory_synopsis')}

    self.errata_cache_expire = \
        datetime.now() + timedelta(self.ERRATA_CACHE_TTL)

    self.save_errata_cache()

    if not self.options.quiet:
        # restore the original line buffer
        self.replace_line_buffer()


def save_errata_cache(self):
    save_cache(self.errata_cache_file,
               self.all_errata,
               self.errata_cache_expire)


def clear_package_cache(self):
    self.all_packages_short = {}
    self.all_packages = {}
    self.all_packages_by_id = {}
    self.package_cache_expire = datetime.now()
    self.save_package_caches()


def generate_package_cache(self, force=False):
    if not force and datetime.now() < self.package_cache_expire:
        return

    if not self.options.quiet:
        # tell the user what's going on
        self.replace_line_buffer('** Generating package cache **')

    channels = self.client.channel.listSoftwareChannels(self.session)
    channels = [c.get('label') for c in channels]

    for c in channels:
        try:
            packages = \
                self.client.channel.software.listAllPackages(self.session, c)
        except xmlrpclib.Fault:
            logging.debug('No access to %s', c)
            continue

        for p in packages:
            if not p.get('name') in self.all_packages_short:
                self.all_packages_short[p.get('name')] = ''

            longname = build_package_names(p)

            if not longname in self.all_packages:
                self.all_packages[longname] = [p.get('id')]
            else:
                self.all_packages[longname].append(p.get('id'))

    # keep a reverse dictionary so we can lookup package names by ID
    # We assume that package IDs are unique, so one ID is only
    # refering one package.
    self.all_packages_by_id = {}
    for k, v in self.all_packages.items():
        for i in v:
            # Alert in case of non-unique ID is detected.
            if i in self.all_packages_by_id:
                logging.debug(
                    'Non-unique package id "%s" is detected. Taking "%s" '
                    'instead of "%s"' % (i, k, self.all_packages_by_id[i]))

            self.all_packages_by_id[i] = k

    self.package_cache_expire = \
        datetime.now() + timedelta(seconds=self.PACKAGE_CACHE_TTL)

    self.save_package_caches()

    if not self.options.quiet:
        # restore the original line buffer
        self.replace_line_buffer()


def save_package_caches(self):
    # store the cache to disk to speed things up
    save_cache(self.packages_short_cache_file,
               self.all_packages_short,
               self.package_cache_expire)

    save_cache(self.packages_long_cache_file,
               self.all_packages,
               self.package_cache_expire)

    save_cache(self.packages_by_id_cache_file,
               self.all_packages_by_id,
               self.package_cache_expire)


# create a global list of all available package names
def get_package_names(self, longnames=False):
    self.generate_package_cache()

    if longnames:
        return self.all_packages.keys()

    return self.all_packages_short


def get_package_id(self, name):
    self.generate_package_cache()

    try:
        return set(self.all_packages[name])
    except TypeError:
        # FIX: If we're using an old style cache (package_name -> integer_id)
        # then we insert the integer id into a set.
        return set([self.all_packages[name]])
    except KeyError:
        return


def get_package_name(self, package_id):
    self.generate_package_cache()

    try:
        return self.all_packages_by_id[package_id]
    except KeyError:
        return


def clear_system_cache(self):
    self.all_systems = {}
    self.system_cache_expire = datetime.now()
    self.save_system_cache()


def generate_system_cache(self, force=False, delay=0):
    if not force and datetime.now() < self.system_cache_expire:
        return

    if not self.options.quiet:
        # tell the user what's going on
        self.replace_line_buffer('** Generating system cache **')

    # we might need to wait for some systems to delete
    if delay:
        sleep(delay)

    systems = self.client.system.listSystems(self.session)

    self.all_systems = {}
    for s in systems:
        self.all_systems[s.get('id')] = s.get('name')

    self.system_cache_expire = \
        datetime.now() + timedelta(seconds=self.SYSTEM_CACHE_TTL)

    self.save_system_cache()

    if not self.options.quiet:
        # restore the original line buffer
        self.replace_line_buffer()


def save_system_cache(self):
    save_cache(self.system_cache_file,
               self.all_systems,
               self.system_cache_expire)


def load_caches(self, server, username):
    conf_dir = os.path.join(self.conf_dir, server, username)

    try:
        if not os.path.isdir(conf_dir):
            os.mkdir(conf_dir, int('0700', 8))
    except OSError:
        logging.error('Could not create directory %s', conf_dir)
        return

    self.ssm_cache_file = os.path.join(conf_dir, 'ssm')
    self.system_cache_file = os.path.join(conf_dir, 'systems')
    self.errata_cache_file = os.path.join(conf_dir, 'errata')
    self.packages_long_cache_file = os.path.join(conf_dir, 'packages_long')
    self.packages_by_id_cache_file = \
        os.path.join(conf_dir, 'packages_by_id')
    self.packages_short_cache_file = \
        os.path.join(conf_dir, 'packages_short')

    # load self.ssm from disk
    (self.ssm, _ignore) = load_cache(self.ssm_cache_file)

    # update the prompt now that we loaded the SSM
    self.postcmd(False, '')

    # load self.all_systems from disk
    (self.all_systems, self.system_cache_expire) = \
        load_cache(self.system_cache_file)

    # load self.all_errata from disk
    (self.all_errata, self.errata_cache_expire) = \
        load_cache(self.errata_cache_file)

    # load self.all_packages_short from disk
    (self.all_packages_short, self.package_cache_expire) = \
        load_cache(self.packages_short_cache_file)

    # load self.all_packages from disk
    (self.all_packages, self.package_cache_expire) = \
        load_cache(self.packages_long_cache_file)

    # load self.all_packages_by_id from disk
    (self.all_packages_by_id, self.package_cache_expire) = \
        load_cache(self.packages_by_id_cache_file)


def get_system_names(self):
    self.generate_system_cache()
    return self.all_systems.values()


def get_system_names_ids(self):
    self.generate_system_cache()
    return self.all_systems


# check for duplicate system names and return the system ID
def get_system_id(self, name):
    name = "%s" % name
    self.generate_system_cache()
    systems = []

    try:
        # check if we were passed a system instead of a name
        system_id = int(name)
        if system_id in self.all_systems:
            systems.append(system_id)
    except ValueError:
        pass

    # get a set of matching systems to check for duplicate names
    if not systems:
        for system_id in self.all_systems:
            if name == self.all_systems[system_id]:
                systems.append(system_id)

    if len(systems) == 1:
        return systems[0]
    elif not systems:
        logging.warning("Can't find system ID for %s", name)
        return 0
    else:
        if len(systems) == 2 and systems[0] == systems[1]:
            return systems[0]
        logging.warning('Duplicate system profile names found!')
        logging.warning("Please reference systems by ID or resolve the")
        logging.warning("underlying issue with 'system_delete' or 'system_rename'")

        id_list = '%s = ' % name

        for system_id in systems:
            id_list = id_list + '%i, ' % system_id

        logging.warning('')
        logging.warning(id_list[:-2])

        return 0


def get_system_name(self, system_id):
    self.generate_system_cache()

    try:
        return self.all_systems[system_id]
    except KeyError:
        return


def get_org_id(self, name):
    details = self.client.org.getDetails(self.session, name)
    return details.get('id')


def expand_errata(self, args):
    if not isinstance(args, list):
        args = args.split()

    self.generate_errata_cache()

    if not args:
        return self.all_errata

    errata = []
    for item in args:
        if re.match('search:', item):
            item = re.sub('search:', '', item)
            errata.extend(self.do_errata_search(item, True))
        else:
            errata.append(item)

    matches = filter_results(self.all_errata, errata)

    return matches


def expand_systems(self, args):
    if not isinstance(args, list):
        args = shlex.split(args)

    systems = []
    system_ids = []

    for item in args:
        if re.match('ssm', item, re.I):
            systems.extend(self.ssm)
        elif re.match('group:', item):
            item = re.sub('group:', '', item)
            members = self.do_group_listsystems("'%s'" % item, True)

            if members:
                systems.extend([re.escape(m) for m in members])
            else:
                logging.warning('No systems in group %s', item)
        elif re.match('search:', item):
            query = item.split(':', 1)[1]
            results = self.do_system_search(query, True)

            if results:
                system_ids.extend(results)
        elif re.match('channel:', item):
            item = re.sub('channel:', '', item)
            members = self.do_softwarechannel_listsystems(item, True)

            if members:
                systems.extend([re.escape(m) for m in members])
            else:
                logging.warning('No systems subscribed to %s', item)
        else:
            # translate system IDs that the user passes
            try:
                sys_id = int(item)
                system_ids.append(sys_id)
            except ValueError:
                # just a system name
                systems.append(item)

    matches = filter_results(self.get_system_names(), systems)

    return ["%s" % x for x in list(set(matches + system_ids))]


def list_base_channels(self):
    all_channels = self.client.channel.listSoftwareChannels(self.session)

    base_channels = []
    for c in all_channels:
        if not c.get('parent_label'):
            base_channels.append(c.get('label'))

    return base_channels


def list_child_channels(self, system=None, parent=None, subscribed=False):
    channels = []

    if system:
        system_id = self.get_system_id(system)
        if not system_id:
            return

        if subscribed:
            channels = \
                self.client.system.listSubscribedChildChannels(self.session,
                                                               system_id)
        else:
            channels = self.client.system.listSubscribableChildChannels(
                self.session, system_id)
    elif parent:
        all_channels = \
            self.client.channel.listSoftwareChannels(self.session)

        for c in all_channels:
            if parent == c.get('parent_label'):
                channels.append(c)
    else:
        # get all channels that have a parent
        all_channels = \
            self.client.channel.listSoftwareChannels(self.session)

        for c in all_channels:
            if c.get('parent_label'):
                channels.append(c)

    return [c.get('label') for c in channels]


def user_confirm(self, prompt='Is this ok [y/N]:', nospacer=False,
                 integer=False, ignore_yes=False):

    if self.options.yes and not ignore_yes:
        return True

    if nospacer:
        answer = prompt_user('%s' % prompt)
    else:
        answer = prompt_user('\n%s' % prompt)

    if re.match('y', answer, re.I):
        if integer:
            return 1

        return True

    if integer:
        return 0

    return False


# check if the available API is recent enough
def check_api_version(self, want):
    want_parts = [int(i) for i in want.split('.')]
    have_parts = [int(i) for i in self.api_version.split('.')]

    if len(have_parts) == 2 and len(want_parts) == 2:
        if have_parts[0] == want_parts[0]:
            # compare minor versions if majors are the same
            return have_parts[1] >= want_parts[1]

        # only compare major versions if they differ
        return have_parts[0] >= want_parts[0]

    # compare the whole value
    return float(self.api_version) >= float(want)


# replace the current line buffer
def replace_line_buffer(self, msg=None):
    # restore the old buffer if we weren't given a new line
    if not msg:
        msg = readline.get_line_buffer()

    # don't print(a prompt if there wasn't one to begin with)
    if readline.get_line_buffer():
        new_line = '%s%s' % (self.prompt, msg)
    else:
        new_line = '%s' % msg

    # clear the current line
    self.stdout.write('\r'.ljust(len(self.current_line) + 1))
    self.stdout.flush()

    # write the new line
    self.stdout.write('\r%s' % new_line)
    self.stdout.flush()

    # keep track of what is displayed so we can clear it later
    self.current_line = new_line


def load_config_section(self, section):
    config_opts = ['server', 'username', 'password', 'nossl']

    if not self.config_parser.has_section(section):
        logging.debug('Configuration section [%s] does not exist', section)
        return

    logging.debug('Loading configuration section [%s]', section)

    for key in config_opts:
        # don't override command-line options
        if self.options.__dict__[key]:
            # set the config value to the command-line argument
            self.config[key] = self.options.__dict__[key]
        else:
            try:
                self.config[key] = self.config_parser.get(section, key)
            except NoOptionError:
                pass

    try:
        if ('username' in self.config
                and self.config['username'] != self.config_parser.get(section, 'username')):
            del self.config['password']
    except NoOptionError:
        pass

    # handle the nossl boolean
    if 'nossl' in self.config and isinstance(self.config['nossl'], str):
        self.config['nossl'] = re.match('^1|y|true$', self.config['nossl'], re.I)

    # Obfuscate the password with asterisks
    config_debug = self.config.copy()
    if 'password' in config_debug:
        config_debug['password'] = "*" * len(config_debug['password'])

    logging.debug('Current Configuration: %s', config_debug)
07070100000020000081B40000000000000000000000015DA8415F00002F41000000000000000000000000000000000000001D00000000spacecmd/src/spacecmd/org.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import logging
from getpass import getpass
from operator import itemgetter
from spacecmd.utils import *

_PREFIXES = ['Dr.', 'Mr.', 'Miss', 'Mrs.', 'Ms.']


def help_org_create(self):
    print('org_create: Create an organization')
    print('''usage: org_create [options])

options:
  -n ORG_NAME
  -u USERNAME
  -P PREFIX (%s)
  -f FIRST_NAME
  -l LAST_NAME
  -e EMAIL
  -p PASSWORD
  --pam enable PAM authentication''' % ', '.join(_PREFIXES))


def do_org_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--org-name')
    arg_parser.add_argument('-u', '--username')
    arg_parser.add_argument('-P', '--prefix')
    arg_parser.add_argument('-f', '--first-name')
    arg_parser.add_argument('-l', '--last-name')
    arg_parser.add_argument('-e', '--email')
    arg_parser.add_argument('-p', '--password')
    arg_parser.add_argument('--pam', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.org_name = prompt_user('Organization Name:', noblank=True)
        options.username = prompt_user('Username:', noblank=True)
        options.prefix = prompt_user('Prefix (%s):' % ', '.join(_PREFIXES),
                                     noblank=True)
        options.first_name = prompt_user('First Name:', noblank=True)
        options.last_name = prompt_user('Last Name:', noblank=True)
        options.email = prompt_user('Email:', noblank=True)
        options.pam = self.user_confirm('PAM Authentication [y/N]:',
                                        nospacer=True,
                                        integer=False,
                                        ignore_yes=True)

        options.password = ''
        while options.password == '':
            password1 = getpass('Password: ')
            password2 = getpass('Repeat Password: ')

            if password1 == password2:
                options.password = password1
            elif password1 == '':
                logging.warning('Password must be at least 5 characters')
            else:
                logging.warning("Passwords don't match")
    else:
        if not options.org_name:
            logging.error('An organization name is required')
            return

        if not options.username:
            logging.error('A username is required')
            return

        if not options.first_name:
            logging.error('A first name is required')
            return

        if not options.last_name:
            logging.error('A last name is required')
            return

        if not options.email:
            logging.error('An email address is required')
            return

        if not options.password:
            logging.error('A password is required')
            return

        if not options.pam:
            options.pam = False

        if not options.prefix:
            options.prefix = 'Dr.'

    if options.prefix[-1] != '.' and options.prefix != 'Miss':
        options.prefix = options.prefix + '.'

    self.client.org.create(self.session,
                           options.org_name,
                           options.username,
                           options.password,
                           options.prefix.capitalize(),
                           options.first_name,
                           options.last_name,
                           options.email,
                           options.pam)

####################


def help_org_delete(self):
    print('org_delete: Delete an organization')
    print('usage: org_delete NAME')


def complete_org_delete(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_org_delete()
        return

    name = args[0]
    org_id = self.get_org_id(name)

    if self.user_confirm('Delete this organization [y/N]:'):
        self.client.org.delete(self.session, org_id)

####################


def help_org_rename(self):
    print('org_rename: Rename an organization')
    print('usage: org_rename OLDNAME NEWNAME')


def complete_org_rename(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_rename(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_org_rename()
        return

    org_id = self.get_org_id(args[0])
    new_name = args[1]

    self.client.org.updateName(self.session, org_id, new_name)

####################


def help_org_addtrust(self):
    print('org_addtrust: Add a trust between two organizations')
    print('usage: org_addtrust YOUR_ORG ORG_TO_TRUST')


def complete_org_addtrust(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_addtrust(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_org_addtrust()
        return

    your_org_id = self.get_org_id(args[0])
    org_to_trust_id = self.get_org_id(args[1])

    self.client.org.trusts.addTrust(self.session, your_org_id, org_to_trust_id)

####################


def help_org_removetrust(self):
    print('org_removetrust: Remove a trust between two organizations')
    print('usage: org_removetrust YOUR_ORG TRUSTED_ORG')


def complete_org_removetrust(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_removetrust(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_org_removetrust()
        return

    your_org_id = self.get_org_id(args[0])
    trusted_org_id = self.get_org_id(args[1])

    systems = self.client.org.trusts.listSystemsAffected(self.session,
                                                         your_org_id,
                                                         trusted_org_id)

    print('Affected Systems')
    print('----------------')

    if systems:
        print('\n'.join(sorted([s.get('systemName') for s in systems])))
    else:
        print('None')

    if not self.user_confirm('Remove this trust [y/N]:'):
        return

    self.client.org.trusts.removeTrust(self.session,
                                       your_org_id,
                                       trusted_org_id)

####################


def help_org_trustdetails(self):
    print('org_trustdetails: Show the details of an organizational trust')
    print('usage: org_trustdetails TRUSTED_ORG')


def complete_org_trustdetails(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_trustdetails(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_org_trustdetails()
        return

    trusted_org = args[0]
    org_id = self.get_org_id(trusted_org)

    details = self.client.org.trusts.getDetails(self.session, org_id)
    consumed = self.client.org.trusts.listChannelsConsumed(self.session, org_id)
    provided = self.client.org.trusts.listChannelsProvided(self.session, org_id)

    print('Trusted Organization:   %s' % trusted_org)
    print('Trusted Since:          %s' % details.get('trusted_since'))
    print('Systems Migrated From:  %i' % details.get('systems_migrated_from'))
    print('Systems Migrated To:    %i' % details.get('systems_migrated_to'))
    print('')
    print('Channels Consumed')
    print('-----------------')
    if consumed:
        print('\n'.join(sorted([c.get('name') for c in consumed])))

    print('')

    print('Channels Provided')
    print('-----------------')
    if provided:
        print('\n'.join(sorted([c.get('name') for c in provided])))

####################


def help_org_list(self):
    print('org_list: List all organizations')
    print('usage: org_list')


def do_org_list(self, args, doreturn=False):
    orgs = self.client.org.listOrgs(self.session)
    orgs = [o.get('name') for o in orgs]

    if doreturn:
        return orgs
    else:
        if orgs:
            print('\n'.join(sorted(orgs)))

####################


def help_org_listtrusts(self):
    print("org_listtrusts: List an organization's trusts")
    print('usage: org_listtrusts NAME')


def complete_org_listtrusts(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_listtrusts(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_org_listtrusts()
        return

    org_id = self.get_org_id(args[0])

    trusts = self.client.org.trusts.listTrusts(self.session, org_id)

    for trust in sorted(trusts, key=itemgetter('orgName')):
        if trust.get('trustEnabled'):
            print(trust.get('orgName'))

####################


def help_org_listusers(self):
    print("org_listusers: List an organization's users")
    print('usage: org_listusers NAME')


def complete_org_listusers(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_listusers(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_org_listusers()
        return

    org_id = self.get_org_id(args[0])

    users = self.client.org.listUsers(self.session, org_id)

    print('\n'.join(sorted([u.get('login') for u in users])))

####################


def help_org_details(self):
    print('org_details: Show the details of an organization')
    print('usage: org_details NAME')


def complete_org_details(self, text, line, beg, end):
    return tab_completer(self.do_org_list('', True), text)


def do_org_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_org_details()
        return

    name = args[0]

    details = self.client.org.getDetails(self.session, name)

    print('Name:                   %s' % details.get('name'))
    print('Active Users:           %i' % details.get('active_users'))
    print('Systems:                %i' % details.get('systems'))

    # trusts is optional, which is annoying...
    if 'trusts' in details:
        print('Trusts:                 %i' % details.get('trusts'))
    else:
        print('Trusts:                 %i' % 0)

    print('System Groups:          %i' % details.get('system_groups'))
    print('Activation Keys:        %i' % details.get('activation_keys'))
    print('Kickstart Profiles:     %i' % details.get('kickstart_profiles'))
    print('Configuration Channels: %i' % details.get('configuration_channels'))


    (args, _options) = parse_command_arguments(args, arg_parser)
07070100000021000081B40000000000000000000000015DA8415F00002C73000000000000000000000000000000000000002100000000spacecmd/src/spacecmd/package.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_package_details(self):
    print('package_details: Show the details of a software package')
    print('usage: package_details PACKAGE ...')


def complete_package_details(self, text, line, beg, end):
    return tab_completer(self.get_package_names(True), text)


def do_package_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_package_details()
        return

    packages = []
    for package in args:
        packages.extend(self.do_package_search(' '.join(args), True))

    if not packages:
        logging.warning('No packages found')
        return

    add_separator = False

    for package in packages:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        package_ids = self.get_package_id(package)

        if not package_ids:
            logging.warning('%s is not a valid package' % package)
            continue

        for package_id in package_ids:
            details = self.client.packages.getDetails(self.session, package_id)

            channels = \
                self.client.packages.listProvidingChannels(self.session, package_id)

            installed_systems = \
                self.client.system.listSystemsWithPackage(self.session, package_id)

            print('Name:    %s' % details.get('name'))
            print('Version: %s' % details.get('version'))
            print('Release: %s' % details.get('release'))
            print('Epoch:   %s' % details.get('epoch'))
            print('Arch:    %s' % details.get('arch_label'))
            print('')
            print('File:    %s' % details.get('file'))
            print('Path:    %s' % details.get('path'))
            print('Size:    %s' % details.get('size'))
            print('%s:  %s' % (details.get('checksum_type').upper(), details.get('checksum')))
            print('')
            print('Installed Systems: %i' % len(installed_systems))
            print('')
            print('Description')
            print('-----------')
            print('\n'.join(wrap(details.get('description'))))
            print('')
            print('Available From Channels')
            print('-----------------------')
            print('\n'.join(sorted([c.get('label') for c in channels])))
            print('')

####################


def help_package_search(self):
    print('package_search: Find packages that meet the given criteria')
    print('usage: package_search NAME|QUERY')
    print('')
    print('Example: package_search kernel')
    print('')
    print('Advanced Search:')
    print('Available Fields: name, epoch, version, release, arch, description, summary')
    print('Example: name:kernel AND version:2.6.18 AND -description:devel')


def do_package_search(self, args, doreturn=False):
    if not args:
        self.help_package_search()
        return

    fields = ('name:', 'epoch:', 'version:', 'release:',
              'arch:', 'description:', 'summary:')

    packages = []
    advanced = False

    for f in fields:
        if args.find(f) != -1:
            logging.debug('Using advanced search')
            advanced = True
            break

    if advanced:
        packages = self.client.packages.search.advanced(self.session, args)
        packages = build_package_names(packages)
    else:
        # for non-advanced searches, use local regex instead of
        # the APIs for searching; this is done because the fuzzy
        # search on the server gives a lot of garbage back
        packages = filter_results(self.get_package_names(True),
                                  [args], search=True)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(sorted(packages)))

####################


def help_package_remove(self):
    print('package_remove: Remove a package from Satellite')
    print('usage: package_remove PACKAGE ...')


def complete_package_remove(self, text, line, beg, end):
    return tab_completer(self.get_package_names(True), text)


def do_package_remove(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_package_remove()
        return

    packages = args

    to_remove = filter_results(self.get_package_names(True), packages)

    if not to_remove:
        return

    print('Packages')
    print('--------')
    print('\n'.join(sorted(to_remove)))

    if not self.user_confirm('Remove these packages [y/N]:'):
        return

    for package in to_remove:
        for package_id in self.get_package_id(package):
            try:
                self.client.packages.removePackage(self.session, package_id)
            except xmlrpclib.Fault:
                logging.error('Failed to remove package ID %i' % package_id)

    # regenerate the package cache after removing these packages
    self.generate_package_cache(True)

####################


def help_package_listorphans(self):
    print('package_listorphans: List packages that are not in a channel')
    print('usage: package_listorphans')


def do_package_listorphans(self, args, doreturn=False):
    packages = self.client.channel.software.listPackagesWithoutChannel(
        self.session)

    packages = build_package_names(packages)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(sorted(packages)))

####################


def help_package_removeorphans(self):
    print('package_removeorphans: Remove packages that are not in a channel')
    print('usage: package_removeorphans')


def do_package_removeorphans(self, args):
    packages = \
        self.client.channel.software.listPackagesWithoutChannel(self.session)

    if not packages:
        logging.warning('No orphaned packages')
        return

    print('Packages')
    print('--------')
    print('\n'.join(sorted(build_package_names(packages))))

    if not self.user_confirm('Remove these packages [y/N]:'):
        return

    for package in packages:
        try:
            self.client.packages.removePackage(self.session, package.get('id'))
        except xmlrpclib.Fault:
            logging.error('Failed to remove package ID %i' % package.get('id'))

####################


def help_package_listinstalledsystems(self):
    print('package_listinstalledsystems: List the systems with a package installed')
    print('usage: package_listinstalledsystems PACKAGE ...')


def complete_package_listinstalledsystems(self, text, line, beg, end):
    return tab_completer(self.get_package_names(True), text)


def do_package_listinstalledsystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_package_listinstalledsystems()
        return

    packages = []
    for package in args:
        packages.extend(self.do_package_search(package, True))

    if not packages:
        logging.warning('No packages found')
        return

    add_separator = False

    for package in packages:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        systems = []
        for package_id in self.get_package_id(package):
            systems += self.client.system.listSystemsWithPackage(self.session,
                                                                 package_id)

        print(package)
        print('-' * len(package))

        if systems:
            print('\n'.join(sorted(['%s : %s' % (s.get('name'), s.get('id')) for s in systems])))

####################


def help_package_listerrata(self):
    print('package_listerrata: List the errata that provide this package')
    print('usage: package_listerrata PACKAGE ...')


def complete_package_listerrata(self, text, line, beg, end):
    return tab_completer(self.get_package_names(True), text)


def do_package_listerrata(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_package_listerrata()
        return

    packages = []
    for package in args:
        packages.extend(self.do_package_search(' '.join(args), True))

    if not packages:
        logging.warning('No packages found')
        return

    add_separator = False

    for package in packages:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        for package_id in self.get_package_id(package):
            errata = self.client.packages.listProvidingErrata(self.session,
                                                              package_id)

            print(package)
            print('-' * len(package))

            if errata:
                print('\n'.join(sorted([e.get('advisory') for e in errata])))

####################


def help_package_listdependencies(self):
    print('package_listdependencies: List the dependencies for a package')
    print('usage: package_listdependencies PACKAGE')


def do_package_listdependencies(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_package_listdependencies()
        return

    packages = []
    for package in args:
        packages.extend(self.do_package_search(' '.join(args), True))

    if not packages:
        logging.warning('No packages found')
        return

    add_separator = False

    for package in packages:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        for package_id in self.get_package_id(package):
            if not package_id:
                logging.warning('%s is not a valid package' % package)
                continue

            package_id = int(package_id)
            pkgdeps = self.client.packages.list_dependencies(self.session, package_id)
            print('Package Name: %s' % package)
            for dep in pkgdeps:
                print('Dependency: %s Type: %s Modifier: %s' % \
                      (dep['dependency'], dep['dependency_type'], dep['dependency_modifier']))
            print(self.SEPARATOR)
07070100000022000081B40000000000000000000000015DA8415F00003216000000000000000000000000000000000000001E00000000spacecmd/src/spacecmd/repo.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2011 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import shlex
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *



def help_repo_list(self):
    print('repo_list: List all available user repos')
    print('usage: repo_list')


def do_repo_list(self, args, doreturn=False):
    repos = self.client.channel.software.listUserRepos(self.session)
    repos = [c.get('label') for c in repos]

    if doreturn:
        return repos
    else:
        if repos:
            print('\n'.join(sorted(repos)))

####################


def help_repo_details(self):
    print('repo_details: Show the details of a user repo')
    print('usage: repo_details <repo ...>')


def complete_repo_details(self, text, line, beg, end):
    return tab_completer(self.do_repo_list('', True), text)


def do_repo_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_repo_details()
        return

    # allow globbing of repo names
    repos = filter_results(self.do_repo_list('', True), args)

    add_separator = False

    for repo in repos:
        details = self.client.channel.software.getRepoDetails(
            self.session, repo)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Repository Label:                  %s' % details.get('label'))
        print('Repository URL:                    %s' % details.get('sourceUrl'))
        print('Repository Type:                   %s' % details.get('type'))
        print('Repository SSL Ca Certificate:     %s' % (details.get('sslCaDesc') or "None"))
        print('Repository SSL Client Certificate: %s' % (details.get('sslCertDesc') or "None"))
        print('Repository SSL Client Key:         %s' % (details.get('sslKeyDesc') or "None"))

####################


def help_repo_listfilters(self):
    print('repo_listfilters: Show the filters for a user repo')
    print('usage: repo_listfilters repo')


def complete_repo_listfilters(self, text, line, beg, end):
    return tab_completer(self.do_repo_list('', True), text)


def do_repo_listfilters(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_repo_listfilters()
        return

    filters = \
        self.client.channel.software.listRepoFilters(self.session, args[0])

    for f in filters:
        print("%s%s" % (f.get('flag'), f.get('filter')))

####################


def help_repo_addfilters(self):
    print('repo_addfilters: Add filters for a user repo')
    print('usage: repo_addfilters repo <filter ...>')


def complete_repo_addfilters(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_repo_list('', True),
                             text)


def do_repo_addfilters(self, args):
    # arguments can start with -, so don't parse arguments in the normal way
    args = shlex.split(args)

    if not args:
        self.help_repo_addfilters()
        return

    repo = args[0]

    for arg in args[1:]:
        flag = arg[0]
        repofilter = arg[1:]

        if not (flag == '+' or flag == '-'):
            logging.error('Each filter must start with + or -')
            return

        self.client.channel.software.addRepoFilter(self.session,
                                                   repo,
                                                   {'filter': repofilter,
                                                    'flag': flag})

####################


def help_repo_removefilters(self):
    print('repo_removefilters: Remove filters from a user repo')
    print('usage: repo_removefilters repo <filter ...>')


def complete_repo_removefilters(self, text, line, beg, end):
    return tab_completer(self.do_repo_remove('', True), text)


def do_repo_removefilters(self, args):
    # arguments can start with -, so don't parse arguments in the normal way
    args = shlex.split(args)

    if not args:
        self.help_repo_removefilters()
        return

    repo = args[0]

    for arg in args[1:]:
        flag = arg[0]
        repofilter = arg[1:]

        if not (flag == '+' or flag == '-'):
            logging.error('Each filter must start with + or -')
            return

        self.client.channel.software.removeRepoFilter(self.session,
                                                      repo,
                                                      {'filter': repofilter,
                                                       'flag': flag})

####################


def help_repo_setfilters(self):
    print('repo_setfilters: Set the filters for a user repo')
    print('usage: repo_setfilters repo <filter ...>')


def complete_repo_setfilters(self, text, line, beg, end):
    return tab_completer(self.do_repo_set('', True), text)


def do_repo_setfilters(self, args):
    # arguments can start with -, so don't parse arguments in the normal way
    args = shlex.split(args)

    if not args:
        self.help_repo_setfilters()
        return

    repo = args[0]

    filters = []

    for arg in args[1:]:
        flag = arg[0]
        repofilter = arg[1:]

        if not (flag == '+' or flag == '-'):
            logging.error('Each filter must start with + or -')
            return

        filters.append({'filter': repofilter, 'flag': flag})

    self.client.channel.software.setRepoFilters(self.session, repo, filters)

####################


def help_repo_clearfilters(self):
    print('repo_clearfilters: Clears the filters for a user repo')
    print('usage: repo_clearfilters repo')


def complete_repo_clearfilters(self, text, line, beg, end):
    return tab_completer(self.do_repo_clear('', True), text)


def do_repo_clearfilters(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_repo_clearfilters()
        return

    if self.user_confirm('Remove these filters [y/N]:'):
        self.client.channel.software.clearRepoFilters(self.session, args[0])

####################


def help_repo_delete(self):
    print('repo_delete: Delete a user repo')
    print('usage: repo_delete <repo ...>')


def complete_repo_delete(self, text, line, beg, end):
    return tab_completer(self.do_repo_list('', True), text)


def do_repo_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_repo_delete()
        return

    # allow globbing of repo names
    repos = filter_results(self.do_repo_list('', True), args)

    print('Repos')
    print('-----')
    print('\n'.join(sorted(repos)))

    if self.user_confirm('Delete these repos [y/N]:'):
        for repo in repos:
            try:
                self.client.channel.software.removeRepo(self.session, repo)
            except xmlrpclib.Fault:
                logging.error('Failed to remove repo %s' % repo)

####################


def help_repo_create(self):
    print('repo_create: Create a user repository')
    print('''usage: repo_create <options>)

options:
  -n, --name   name of repository
  -u, --url    url of repository
  -t, --type   type of repository (defaults to yum)

  --ca         SSL CA certificate (not required)
  --cert       SSL Client certificate (not required)
  --key        SSL Client key (not required)''')


def do_repo_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-u', '--url')
    arg_parser.add_argument('-t', '--type')
    arg_parser.add_argument('--ca', default='')
    arg_parser.add_argument('--cert', default='')
    arg_parser.add_argument('--key', default='')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name:', noblank=True)
        options.url = prompt_user('URL:', noblank=True)
        options.type = prompt_user('Type:', noblank=True)
        options.ca = prompt_user('SSL CA cert:')
        options.cert = prompt_user('SSL Client cert:')
        options.key = prompt_user('SSL Client key:')
    else:
        if not options.name:
            logging.error('A name is required')
            return

        if not options.url:
            logging.error('A URL is required')
            return

        if not options.type:
            options.type = 'yum'

    self.client.channel.software.createRepo(self.session,
                                            options.name,
                                            options.type,
                                            options.url,
                                            options.ca,
                                            options.cert,
                                            options.key)

####################


def help_repo_rename(self):
    print('repo_rename: Rename a user repository')
    print('usage: repo_rename OLDNAME NEWNAME')


def complete_repo_rename(self, text, line, beg, end):
    if len(line.split(' ')) <= 2:
        return tab_completer(self.do_repo_list('', True),
                             text)


def do_repo_rename(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_repo_rename()
        return

    try:
        details = self.client.channel.software.getRepoDetails(self.session, args[0])
        oldname = details.get('id')
    except xmlrpclib.Fault:
        logging.error('Could not find repo %s' % args[0])
        return False

    newname = args[1]

    self.client.channel.software.updateRepoLabel(self.session, oldname, newname)

####################


def help_repo_updateurl(self):
    print('repo_updateurl: Change the URL of a user repository')
    print('usage: repo_updateurl <repo> <url>')


def complete_repo_updateurl(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return tab_completer(self.do_repo_list('', True),
                             text)


def do_repo_updateurl(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_repo_updateurl()
        return

    name = args[0]
    url = args[1]

    self.client.channel.software.updateRepoUrl(self.session, name, url)


def help_repo_updatessl(self):
    print('repo_updatessl: Change the SSL certificates of a user repository')
    print('''usage: repo_updatessl <options>)
options:
  --ca         SSL CA certificate (not required)
  --cert       SSL Client certificate (not required)
  --key        SSL Client key (not required)''')


def do_repo_updatessl(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('--ca', default='')
    arg_parser.add_argument('--cert', default='')
    arg_parser.add_argument('--key', default='')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Name:', noblank=True)
        options.ca = prompt_user('SSL CA cert:')
        options.cert = prompt_user('SSL Client cert:')
        options.key = prompt_user('SSL Client key:')
    else:
        if not options.name:
            logging.error('A name is required')
            return

    self.client.channel.software.updateRepoSsl(self.session,
                                               options.name,
                                               options.ca,
                                               options.cert,
                                               options.key)
07070100000023000081B40000000000000000000000015DA8415F00002A21000000000000000000000000000000000000002000000000spacecmd/src/spacecmd/report.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from operator import itemgetter
from spacecmd.utils import *


def help_report_inactivesystems(self):
    print('report_inactivesystems: List all inactive systems')
    print('usage: report_inactivesystems [DAYS]')


def do_report_inactivesystems(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    # allow the user to set a limit on the number of days
    if len(args) == 1:
        try:
            days = int(args[0])
        except ValueError:
            # default to a week when passed a bad argument
            days = 7

        systems = self.client.system.listInactiveSystems(self.session, days)
    else:
        # use the server's default period if no argument was passed
        systems = self.client.system.listInactiveSystems(self.session)

    if systems:
        max_size = max_length([s.get('name') for s in systems])

        print('%s  %s  %s' % ('System ID ', 'System'.ljust(max_size), 'Last Checkin'))
        print(('----------  '+'-' * max_size) + '  ------------')

        for s in sorted(systems, key=itemgetter('name')):
            print('%s  %s  %s' % (s.get('id'),s.get('name').ljust(max_size),
                                  s.get('last_checkin')))

####################


def help_report_outofdatesystems(self):
    print('report_outofdatesystems: List all out-of-date systems')
    print('usage: report_outofdatesystems')


def do_report_outofdatesystems(self, args):
    systems = self.client.system.listOutOfDateSystems(self.session)

    max_size = max_length([s.get('name') for s in systems])

    report = {}
    for system in systems:
        report[system.get('name')] = system.get('outdated_pkg_count')

    if report:
        print('%s  %s' % ('System'.ljust(max_size), 'Packages'))
        print(('-' * max_size) + '  --------')

        for system in sorted(report):
            print('%s       %s' %
                  (system.ljust(max_size), str(report[system]).rjust(3)))

####################


def help_report_ungroupedsystems(self):
    print('report_ungroupedsystems: List all ungrouped systems')
    print('usage: report_ungroupedsystems')


def do_report_ungroupedsystems(self, args):
    systems = self.client.system.listUngroupedSystems(self.session)
    systems = [s.get('name') for s in systems]

    if systems:
        print('\n'.join(sorted(systems)))

####################


def help_report_errata(self):
    print('report_errata: List all errata and how many systems they affect')
    print('usage: report_errata [ERRATA|search:XXX ...]')

# XXX: performance is terrible due to all the API calls


def do_report_errata(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        print('All errata requested - this may take a few minutes, please be patient!')

    errata_list = self.expand_errata(args)

    report = {}
    for erratum in errata_list:
        logging.debug('Getting affected systems for %s' % erratum)

        affected = self.client.errata.listAffectedSystems(self.session, erratum)

        num_affected = len(affected)
        if num_affected:
            report[erratum] = num_affected

    # XXX: max(list, key=len) in >2.5
    max_size = 0
    for e in report:
        size = len(e)
        if size > max_size:
            max_size = size

    if report:
        print('%s  # Systems' % ('Errata'.ljust(max_size)))
        print('%s  ---------' % ('------'.ljust(max_size)))
        for erratum in sorted(report):
            print('%s        %s' %
                  (erratum.ljust(max_size), str(report[erratum]).rjust(3)))

####################


def help_report_ipaddresses(self):
    print('report_ipaddresses: List the hostname and IP of each system')
    print('usage: report_ipaddresses [<SYSTEMS>]')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def do_report_ipaddresses(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        # use the systems listed in the SSM
        if re.match('ssm', args[0], re.I):
            systems = self.ssm.keys()
        else:
            systems = self.expand_systems(args)
    else:
        systems = self.get_system_names()

    report = {}
    for system in systems:
        system_id = self.get_system_id(system)
        network = self.client.system.getNetwork(self.session, system_id)
        report[system] = {'hostname': network.get('hostname'),
                          'ip': network.get('ip')}

    # XXX: max(list, key=len) in >2.5
    system_max_size = 0
    for s in report:
        size = len(s)
        if size > system_max_size:
            system_max_size = size

    hostname_max_size = 0
    for h in [report[h]['hostname'] for h in report]:
        size = len(h)
        if size > hostname_max_size:
            hostname_max_size = size

    if report:
        print('%s  %s  IP' % ('System'.ljust(system_max_size),
                              'Hostname'.ljust(hostname_max_size)))

        print('%s  %s  --' % ('------'.ljust(system_max_size),
                              '--------'.ljust(hostname_max_size)))

        for system in sorted(report):
            print('%s  %s  %s' %
                  (system.ljust(system_max_size),
                   report[system]['hostname'].ljust(hostname_max_size),
                   report[system]['ip'].ljust(15)))

####################


def help_report_kernels(self):
    print('report_kernels: List the running kernel of each system')
    print('usage: report_kernels [<SYSTEMS>]')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def do_report_kernels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if args:
        # use the systems listed in the SSM
        if re.match('ssm', args[0], re.I):
            systems = self.ssm.keys()
        else:
            systems = self.expand_systems(args)
    else:
        systems = self.get_system_names()

    report = {}
    for system in systems:
        system_id = self.get_system_id(system)
        kernel = self.client.system.getRunningKernel(self.session, system_id)
        report[system] = kernel

    # XXX: max(list, key=len) in >2.5
    system_max_size = 0
    for s in report:
        size = len(s)
        if size > system_max_size:
            system_max_size = size

    if report:
        print('%s  Kernel' % ('System'.ljust(system_max_size)))

        print('%s  ------' % ('------'.ljust(system_max_size)))

        for system in sorted(report):
            print('%s  %s' % (system.ljust(system_max_size), report[system]))

####################


def help_report_duplicates(self):
    print('report_duplicates: List duplicate system profiles')
    print('usage: report_duplicates')


def do_report_duplicates(self, args):
    add_separator = False

    dupes_by_profile = []
    for system in self.get_system_names():
        if self.get_system_names().count(system) > 1:
            if system not in dupes_by_profile:
                dupes_by_profile.append(system)

    if dupes_by_profile:
        add_separator = True

        for item in dupes_by_profile:
            print('%s:' % item)

            # get some details for each duplicate
            systems = self.client.system.searchByName(self.session,
                                                      '^%s$' % item)

            print('System ID   Last Checkin')
            print('----------  -----------------')

            for dupe in systems:
                print('%i  %s' % (dupe.get('id'), dupe.get('last_checkin')))

            if len(dupes_by_profile) > 1:
                print('')

    if self.check_api_version('10.11'):
        dupes_by_ip = self.client.system.listDuplicatesByIp(self.session)
        dupes_by_mac = self.client.system.listDuplicatesByMac(self.session)
        dupes_by_hostname = \
            self.client.system.listDuplicatesByHostname(self.session)

        if dupes_by_ip:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            for item in dupes_by_ip:
                print('%s:' % item.get('ip'))

                print('System ID   Last Checkin')
                print('----------  -----------------')

                for dupe in item.get('systems'):
                    print('%i  %s' % (dupe.get('systemId'),
                                      dupe.get('last_checkin')))

                if len(dupes_by_ip) > 1:
                    print('')

        if dupes_by_mac:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            for item in dupes_by_mac:
                print('%s:' % item.get('mac').upper())

                print('System ID   Last Checkin')
                print('----------  -----------------')

                for dupe in item.get('systems'):
                    print('%i  %s' % (dupe.get('systemId'),
                                      dupe.get('last_checkin')))

                if len(dupes_by_mac) > 1:
                    print('')

        if dupes_by_hostname:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            for item in dupes_by_hostname:
                print('%s:' % item.get('hostname'))

                print('System ID   Last Checkin')
                print('----------  -----------------')

                for dupe in item.get('systems'):
                    print('%i  %s' % (dupe.get('systemId'),
                                      dupe.get('last_checkin')))

                if len(dupes_by_hostname) > 1:
                    print('')
07070100000024000081B40000000000000000000000015DA8415F000017B9000000000000000000000000000000000000001E00000000spacecmd/src/spacecmd/scap.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2013--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from spacecmd.utils import *


def help_scap_listxccdfscans(self):
    print('scap_listxccdfscans: Return a list of finished OpenSCAP scans for given systems')
    print('usage: scap_listxccdfscans <SYSTEMS>')


def complete_system_scap_listxccdfscans(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_scap_listxccdfscans(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_scap_listxccdfscans()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in sorted(systems):
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        system_id = self.get_system_id(system)
        if not system_id:
            continue

        scan_list = self.client.system.scap.listXccdfScans(self.session, system_id)

        for s in scan_list:
            print('XID: %d Profile: %s Path: (%s) Completed: %s' % (s['xid'], s['profile'], s['path'], s['completed']))

####################


def help_scap_getxccdfscanruleresults(self):
    print('scap_getxccdfscanruleresults: Return a full list of RuleResults for given OpenSCAP XCCDF scan')
    print('usage: scap_getxccdfscanruleresults <XID>')


def do_scap_getxccdfscanruleresults(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_scap_getxccdfscanruleresults()
        return

    add_separator = False

    for xid in args:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(args) > 1:
            print('XID: %s' % xid)
            print('')

        xid = int(xid)
        scan_results = self.client.system.scap.getXccdfScanRuleResults(self.session, xid)

        for s in scan_results:
            print('IDref: %s Result: %s Idents: (%s)' % (s['idref'], s['result'], s['idents']))

####################


def help_scap_getxccdfscandetails(self):
    print('scap_getxccdfscandetails: Get details of given OpenSCAP XCCDF scan')
    print('usage: scap_getxccdfscandetails <XID>')


def do_scap_getxccdfscandetails(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_scap_getxccdfscandetails()
        return

    add_separator = False

    for xid in args:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(args) > 1:
            print('XID: %s' % xid)
            print('')

        xid = int(xid)
        scan_details = self.client.system.scap.getXccdfScanDetails(self.session, xid)

        print("XID:", scan_details['xid'], "SID:", scan_details['sid'], "Action_ID:",
              scan_details['action_id'], "Path:", scan_details['path'], \
              "OSCAP_Parameters:", scan_details['oscap_parameters'], \
              "Test_Result:", scan_details['test_result'], "Benchmark:", \
              scan_details['benchmark'], "Benchmark_Version:", \
              scan_details['benchmark_version'], "Profile:", scan_details['profile'], \
              "Profile_Title:", scan_details['profile_title'], "Start_Time:", \
              scan_details['start_time'], "End_Time:", scan_details['end_time'], \
              "Errors:", scan_details['errors'])

####################


def help_scap_schedulexccdfscan(self):
    print('scap_schedulexccdfscan: Schedule Scap XCCDF scan')
    print('usage: scap_schedulexccdfscan PATH_TO_XCCDF_FILE XCCDF_OPTIONS SYSTEMS')
    print('       scap_schedulexccdfscan ssm PATH_TO_XCCDF_FILE XCCDF_OPTIONS')
    print('')
    print('Example:')
    print('> scap_schedulexccdfscan \'/usr/share/openscap/scap-security-xccdf.xml\'' +
          ' \'profile Web-Default\' system-scap.example.com')
    print('\nTo use systems in the ssm, pass the "ssm" keyword in front. Example:')
    print("> scap_schedulexccdfscan ssm '/usr/share/openscap/scap-security-xccdf.xml'" +
          " 'profile Web-Default'")


def do_scap_schedulexccdfscan(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_scap_schedulexccdfscan()
        return

    path = args[0]
    param = "--"
    param += args[1]

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args[2:])

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.scap.scheduleXccdfScan(self.session, system_id, path, param)
07070100000025000081B40000000000000000000000015DA8415F000034B3000000000000000000000000000000000000002200000000spacecmd/src/spacecmd/schedule.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import base64
from operator import itemgetter
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def print_schedule_summary(self, action_type, args):
    args = args.split() or []

    if args:
        begin_date = parse_time_input(args[0])
        logging.debug('Begin Date: %s' % begin_date)
    else:
        begin_date = None

    if len(args) > 1:
        end_date = parse_time_input(args[1])
        logging.debug('End Date:   %s' % end_date)
    else:
        end_date = None

    if action_type == 'pending':
        actions = self.client.schedule.listInProgressActions(self.session)
    elif action_type == 'completed':
        actions = self.client.schedule.listCompletedActions(self.session)
    elif action_type == 'failed':
        actions = self.client.schedule.listFailedActions(self.session)
    elif action_type == 'archived':
        actions = self.client.schedule.listArchivedActions(self.session)
    elif action_type == 'all':
        # get actions in all states except archived
        in_progress = self.client.schedule.listInProgressActions(self.session)
        completed = self.client.schedule.listCompletedActions(self.session)
        failed = self.client.schedule.listFailedActions(self.session)

        actions = []
        added = []
        for action in in_progress + completed + failed:
            if action.get('id') not in added:
                actions.append(action)
                added.append(action.get('id'))
    else:
        return

    if not actions:
        return

    print('ID      Date                 C    F    P     Action')
    print('--      ----                ---  ---  ---    ------')

    for action in sorted(actions, key=itemgetter('id'), reverse=True):
        if begin_date:
            if action.get('earliest') < begin_date:
                continue

        if end_date:
            if action.get('earliest') > end_date:
                continue

        if self.check_api_version('10.11'):
            print('%s  %s   %s  %s  %s    %s' %
                  (str(action.get('id')).ljust(6),
                   action.get('earliest'),
                   str(action.get('completedSystems')).rjust(3),
                   str(action.get('failedSystems')).rjust(3),
                   str(action.get('inProgressSystems')).rjust(3),
                   action.get('name')))
        else:
            # Satellite 5.3 compatibility
            in_progress = \
                self.client.schedule.listInProgressSystems(self.session,
                                                           action.get('id'))

            completed = \
                self.client.schedule.listCompletedSystems(self.session,
                                                          action.get('id'))

            failed = \
                self.client.schedule.listFailedSystems(self.session,
                                                       action.get('id'))

            print('%s  %s   %s  %s  %s    %s' %
                  (str(action.get('id')).ljust(6),
                   action.get('earliest'),
                   str(len(completed)).rjust(3),
                   str(len(failed)).rjust(3),
                   str(len(in_progress)).rjust(3),
                   action.get('name')))

####################


def help_schedule_cancel(self):
    print('schedule_cancel: Cancel scheduled actions')
    print('usage: schedule_cancel ID|* ...')


def complete_schedule_cancel(self, text, line, beg, end):
    try:
        actions = self.client.schedule.listInProgressActions(self.session)
        return tab_completer([str(a.get('id')) for a in actions], text)
    except xmlrpclib.Fault:
        return []


def do_schedule_cancel(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_schedule_cancel()
        return

    # cancel all actions
    if '.*' in args:
        if not self.user_confirm('Cancel all pending actions [y/N]:'):
            return

        actions = self.client.schedule.listInProgressActions(self.session)
        strings = [a.get('id') for a in actions]
    else:
        strings = args

    # convert strings to integers
    actions = []
    for a in strings:
        try:
            actions.append(int(a))
        except ValueError:
            logging.warning('%s is not a valid ID' % str(a))
            continue

    self.client.schedule.cancelActions(self.session, actions)

    for a in actions:
        logging.info('Canceled action %i' % a)

    print('Canceled %i action(s)' % len(actions))

####################


def help_schedule_reschedule(self):
    print('schedule_reschedule: Reschedule failed actions')
    print('usage: schedule_reschedule ID|* ...')


def complete_schedule_reschedule(self, text, line, beg, end):
    try:
        actions = self.client.schedule.listFailedActions(self.session)
        return tab_completer([str(a.get('id')) for a in actions], text)
    except xmlrpclib.Fault:
        return []


def do_schedule_reschedule(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_schedule_reschedule()
        return

    failed_actions = self.client.schedule.listFailedActions(self.session)
    failed_actions = [a.get('id') for a in failed_actions]

    to_reschedule = []

    # reschedule all failed actions
    if '.*' in args:
        if not self.user_confirm('Reschedule all failed actions [y/N]:'):
            return
        to_reschedule = failed_actions
    else:
        # use the list of action IDs passed in
        for a in args:
            try:
                action_id = int(a)

                if action_id in failed_actions:
                    to_reschedule.append(action_id)
                else:
                    logging.warning('%i is not a failed action' % action_id)
            except ValueError:
                logging.warning('%s is not a valid ID' % str(a))
                continue

    if not to_reschedule:
        logging.warning('No failed actions to reschedule')
        return

    self.client.schedule.rescheduleActions(self.session, to_reschedule, True)

    print('Rescheduled %i action(s)' % len(to_reschedule))

####################


def help_schedule_details(self):
    print('schedule_details: Show the details of a scheduled action')
    print('usage: schedule_details ID')


def do_schedule_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_schedule_details()
        return

    try:
        action_id = int(args[0])
    except ValueError:
        logging.warning('%s is not a valid ID' % str(action_id))
        return

    completed = self.client.schedule.listCompletedSystems(self.session,
                                                          action_id)

    failed = self.client.schedule.listFailedSystems(self.session, action_id)

    pending = self.client.schedule.listInProgressSystems(self.session,
                                                         action_id)

    # put all the system arrays together for the summary
    all_systems = []
    all_systems.extend(completed)
    all_systems.extend(failed)
    all_systems.extend(pending)

    # schedule.getAction() API call would make this easier
    all_actions = self.client.schedule.listAllActions(self.session)
    action = None
    for a in all_actions:
        if a.get('id') == action_id:
            action = a
            del all_actions
            break

    print('ID:        %i' % action.get('id'))
    print('Action:    %s' % action.get('name'))
    print('User:      %s' % action.get('scheduler'))
    print('Date:      %s' % action.get('earliest'))
    print('')
    print('Completed: %s' % str(len(completed)).rjust(3))
    print('Failed:    %s' % str(len(failed)).rjust(3))
    print('Pending:   %s' % str(len(pending)).rjust(3))

    if completed:
        print('')
        print('Completed Systems')
        print('-----------------')
        for s in completed:
            print(s.get('server_name'))

    if failed:
        print('')
        print('Failed Systems')
        print('--------------')
        for s in failed:
            print(s.get('server_name'))

    if pending:
        print('')
        print('Pending Systems')
        print('---------------')
        for s in pending:
            print(s.get('server_name'))

####################


def help_schedule_getoutput(self):
    print('schedule_getoutput: Show the output from an action')
    print('usage: schedule_getoutput ID')


def do_schedule_getoutput(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_schedule_getoutput()
        return

    try:
        action_id = int(args[0])
    except ValueError:
        logging.error('%s is not a valid action ID' % str(args[0]))
        return

    script_results = None
    try:
        script_results = \
            self.client.system.getScriptResults(self.session, action_id)
    except xmlrpclib.Fault:
        pass

    # scripts have a different data structure than other actions
    if script_results:
        add_separator = False
        for r in script_results:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            if r.get('serverId'):
                system = self.get_system_name(r.get('serverId'))
            else:
                system = 'UNKNOWN'

            print('System:      %s' % system)
            print('Start Time:  %s' % r.get('startDate'))
            print('Stop Time:   %s' % r.get('stopDate'))
            print('Return Code: %i' % r.get('returnCode'))
            print('')
            print('Output')
            print('------')
            if r.get('output_enc64'):
                print(base64.b64decode(r.get('output')))
            else:
                print(r.get('output').encode('UTF8'))

    else:
        completed = self.client.schedule.listCompletedSystems(self.session,
                                                              action_id)

        failed = self.client.schedule.listFailedSystems(self.session,
                                                        action_id)

        add_separator = False

        for action in completed + failed:
            if add_separator:
                print(self.SEPARATOR)
            add_separator = True

            print('System:    %s' % action.get('server_name'))
            print('Completed: %s' % action.get('timestamp'))
            print('')
            print('Output')
            print('------')
            print(action.get('message'))

####################


def help_schedule_listpending(self):
    print('schedule_listpending: List pending actions')
    print('usage: schedule_listpending [BEGINDATE] [ENDDATE]')
    print('')
    print(self.HELP_TIME_OPTS)


def do_schedule_listpending(self, args):
    return self.print_schedule_summary('pending', args)

####################


def help_schedule_listcompleted(self):
    print('schedule_listcompleted: List completed actions')
    print('usage: schedule_listcompleted [BEGINDATE] [ENDDATE]')
    print('')
    print(self.HELP_TIME_OPTS)


def do_schedule_listcompleted(self, args):
    return self.print_schedule_summary('completed', args)

####################


def help_schedule_listfailed(self):
    print('schedule_listfailed: List failed actions')
    print('usage: schedule_listfailed [BEGINDATE] [ENDDATE]')
    print('')
    print(self.HELP_TIME_OPTS)


def do_schedule_listfailed(self, args):
    return self.print_schedule_summary('failed', args)

####################


def help_schedule_listarchived(self):
    print('schedule_listarchived: List archived actions')
    print('usage: schedule_listarchived [BEGINDATE] [ENDDATE]')
    print('')
    print(self.HELP_TIME_OPTS)


def do_schedule_listarchived(self, args):
    return self.print_schedule_summary('archived', args)

####################


def help_schedule_list(self):
    print('schedule_list: List all actions')
    print('usage: schedule_list [BEGINDATE] [ENDDATE]')
    print('')
    print(self.HELP_TIME_OPTS)


def do_schedule_list(self, args):
    return self.print_schedule_summary('all', args)
07070100000026000081B40000000000000000000000015DA8415F00001E11000000000000000000000000000000000000001F00000000spacecmd/src/spacecmd/shell.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

# use of exec
# pylint: disable=W0122

import atexit
import logging
import os
import readline
import re
import shlex
import sys
from cmd import Cmd
from spacecmd.utils import *

class UnknownCallException(Exception):

    def __init__(self):
        Exception.__init__(self)


class SpacewalkShell(Cmd):
    __module_list = ['activationkey', 'configchannel', 'cryptokey',
                     'custominfo', 'distribution', 'errata',
                     'filepreservation', 'group', 'kickstart',
                     'misc', 'org', 'package', 'repo', 'report', 'schedule',
                     'snippet', 'softwarechannel', 'ssm', 'api',
                     'system', 'user', 'utils', 'scap']

    # a SyntaxError is thrown if we don't wrap this in an 'exec'
    for module in __module_list:
        exec('from spacecmd.%s import *' % module)

    # maximum length of history file
    HISTORY_LENGTH = 1024

    cmdqueue = []
    completekey = 'tab'
    stdout = sys.stdout
    prompt_template = 'spacecmd {SSM:##}> '
    current_line = ''

    # do nothing on an empty line
    emptyline = lambda self: None

    def __init__(self, options, conf_dir, config_parser):
        Cmd.__init__(self)

        self.session = ''
        self.current_user = ''
        self.server = ''
        self.ssm = {}
        self.config = {}

        self.postcmd(False, '')

        # make the options available everywhere
        self.options = options

        # make the configuration file available everywhere
        self.config_parser = config_parser

        # this is used when loading and saving caches
        self.conf_dir = conf_dir

        self.history_file = os.path.join(self.conf_dir, 'history')

        try:
            # don't split on hyphens or colons during tab completion
            newdelims = readline.get_completer_delims()
            newdelims = re.sub(':|-|/', '', newdelims)
            readline.set_completer_delims(newdelims)

            if not options.nohistory:
                try:
                    if os.path.isfile(self.history_file):
                        readline.read_history_file(self.history_file)

                    readline.set_history_length(self.HISTORY_LENGTH)

                    # always write the history file on exit
                    atexit.register(readline.write_history_file,
                                    self.history_file)
                except IOError:
                    logging.error('Could not read history file')
        # pylint: disable=W0702
        except Exception as exc:
            # pylint: disable=W0702
            logging.error("Exception occurred: {}".format(exc))
            sys.exit(1)

    # handle shell exits and history substitution
    def precmd(self, line):
        # disable too-many-return-statements warning
        # pylint: disable=R0911

        # remove leading/trailing whitespace
        line = re.sub(r'^\s+|\s+$', '', line)

        # don't do anything on empty lines
        if line == '':
            return ''

        # terminate the shell
        if re.match('quit|exit|eof', line, re.I):
            print('')
            sys.exit(0)

        # don't attempt to login for some commands
        if re.match('help|login|logout|whoami|history|clear', line, re.I):
            # login required for clear_caches or it fails with:
            # "SpacewalkShell instance has no attribute 'system_cache_file'"
            if not re.match('clear_caches', line, re.I):
                return line

        # login before attempting to run a command
        if not self.session:
            # disable no-member error message
            # pylint: disable=E1101
            self.do_login('')
            if self.session == '':
                return ''

        parts = shlex.split(line)

        if parts:
            command = parts[0]
        else:
            return ''

        # print(the help message for a command if the user passed --help)
        if '--help' in parts or '-h' in parts:
            return 'help %s' % command

        # should we look for an item in the history?
        if command[0] != '!' or len(command) < 2:
            return line

        # remove the '!*' line from the history
        # disable no-member error message
        # pylint: disable=E1101
        self.remove_last_history_item()

        history_match = False

        if command[1] == '!':
            # repeat the last command
            line = readline.get_history_item(
                readline.get_current_history_length())
            if line:
                history_match = True
            else:
                logging.warning('%s: event not found', command)
                return ''

        # attempt to find a numbered history item
        if not history_match:
            try:
                number = int(command[1:])
                line = readline.get_history_item(number)
                if line:
                    history_match = True
                else:
                    raise Exception
            except IndexError:
                pass
            except ValueError:
                pass

        # attempt to match the beginning of the string with a history item
        if not history_match:
            history_range = range(1, readline.get_current_history_length())
            history_range.reverse()

            for i in history_range:
                item = readline.get_history_item(i)
                if re.match(command[1:], item):
                    line = item
                    history_match = True
                    break

        # append the arguments to the substituted command
        if history_match:
            if parts[1:]:
                for arg in parts[1:]:
                    line += " '%s'" % arg

            readline.add_history(line)
            print(line)
            return line
        else:
            logging.warning('%s: event not found', command)
            return ''

    @staticmethod
    def print_result(cmdresult, cmd):
        logging.debug(cmd + ": " + repr(cmdresult))
        if cmd:
            try:
                if type(cmdresult).__name__ == 'str':
                    print(cmdresult)
                else:
                    for i in cmdresult:
                        print(i)
            except TypeError:
                pass

    # update the prompt with the SSM size
    # pylint: disable=arguments-differ
    def postcmd(self, cmdresult, cmd):
        SpacewalkShell.print_result(cmdresult, cmd)
        self.prompt = re.sub('##', str(len(self.ssm)), self.prompt_template)

    def default(self, line):
        Cmd.default(self, line)
        raise UnknownCallException
07070100000027000081B40000000000000000000000015DA8415F00001661000000000000000000000000000000000000002100000000spacecmd/src/spacecmd/snippet.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from spacecmd.utils import *


def help_snippet_list(self):
    print('snippet_list: List the available Kickstart snippets')
    print('usage: snippet_list')


def do_snippet_list(self, args, doreturn=False):
    snippets = self.client.kickstart.snippet.listCustom(self.session)
    snippets = sorted([s.get('name') for s in snippets])

    if doreturn:
        return snippets
    else:
        if snippets:
            print('\n'.join(snippets))

####################


def help_snippet_details(self):
    print('snippet_details: Show the contents of a snippet')
    print('usage: snippet_details SNIPPET ...')


def complete_snippet_details(self, text, line, beg, end):
    return tab_completer(self.do_snippet_list('', True),
                         text)


def do_snippet_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_snippet_details()
        return

    add_separator = False

    snippets = self.client.kickstart.snippet.listCustom(self.session)

    snippet = None
    for name in args:
        for s in snippets:
            if s.get('name') == name:
                snippet = s
                break

        if not snippet:
            logging.warning('%s is not a valid snippet' % name)
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Name:   %s' % snippet.get('name'))
        print('Macro:  %s' % snippet.get('fragment'))
        print('File:   %s' % snippet.get('file'))

        print('')
        print(snippet.get('contents'))

####################


def help_snippet_create(self):
    print('snippet_create: Create a Kickstart snippet')
    print('''usage: snippet_create [options])

options:
  -n NAME
  -f FILE''')


def do_snippet_create(self, args, update_name=''):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    contents = ''

    if is_interactive(options):
        # if update_name was passed, we're trying to update an existing snippet
        if update_name:
            options.name = update_name

            snippets = self.client.kickstart.snippet.listCustom(self.session)
            for s in snippets:
                if s.get('name') == update_name:
                    contents = s.get('contents')
                    break

        if not options.name:
            options.name = prompt_user('Name:', noblank=True)

        if self.user_confirm('Read an existing file [y/N]:',
                             nospacer=True, ignore_yes=True):
            options.file = prompt_user('File:')
        else:
            (contents, _ignore) = editor(template=contents,
                                         delete=True)
    else:
        if not options.name:
            logging.error('A name is required for the snippet')
            return

        if not options.file:
            logging.error('A file is required')
            return

    if options.file:
        contents = read_file(options.file)

    print('')
    print('Snippet: %s' % options.name)
    print('Contents')
    print('--------')
    print(contents)

    if self.user_confirm():
        self.client.kickstart.snippet.createOrUpdate(self.session,
                                                     options.name,
                                                     contents)

####################


def help_snippet_update(self):
    print('snippet_update: Update a Kickstart snippet')
    print('usage: snippet_update NAME')


def complete_snippet_update(self, text, line, beg, end):
    return tab_completer(self.do_snippet_list('', True), text)


def do_snippet_update(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_snippet_update()
        return

    return self.do_snippet_create('', update_name=args[0])

####################


def help_snippet_delete(self):
    print('snippet_delete: Delete a Kickstart snippet')
    print('usage: snippet_delete NAME')


def complete_snippet_delete(self, text, line, beg, end):
    return tab_completer(self.do_snippet_list('', True), text)


def do_snippet_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_snippet_delete()
        return

    snippet = args[0]

    if self.user_confirm('Remove this snippet [y/N]:'):
        self.client.kickstart.snippet.delete(self.session, snippet)
07070100000028000081B40000000000000000000000015DA8415F000180DC000000000000000000000000000000000000002900000000spacecmd/src/spacecmd/softwarechannel.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

try:
    from urllib import ContentTooShortError
except ImportError:
    from urllib.error import ContentTooShortError
try:
    from urllib import urlretrieve
except ImportError:
    from urllib.request import urlretrieve
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


CHECKSUM = ['sha1', 'sha256', 'sha384', 'sha512']

def help_softwarechannel_list(self):
    print('softwarechannel_list: List all available software channels')
    print('''usage: softwarechannel_list [options]')
options:
  -v verbose (display label and summary)
  -t tree view (pretty-print(child-channels))
''')


def do_softwarechannel_list(self, args, doreturn=False):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-v', '--verbose', action='store_true')
    arg_parser.add_argument('-t', '--tree', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if (options.tree):
        labels = self.list_base_channels()
    else:
        channels = self.client.channel.listAllChannels(self.session)
        labels = [c.get('label') for c in channels]

    # filter the list if arguments were passed
    if args:
        labels = filter_results(labels, args, True)

    if doreturn:
        return labels
    elif labels:
        if (options.verbose):
            for l in sorted(labels):
                details = self.client.channel.software.getDetails(
                    self.session, l)
                print("%s : %s" % (l, details['summary']))
                if (options.tree):
                    for c in self.list_child_channels(parent=l):
                        cdetails = self.client.channel.software.getDetails(
                            self.session, c)
                        print(" |-%s : %s" % (c, cdetails['summary']))
        else:
            for l in sorted(labels):
                print("%s" % l)
                if (options.tree):
                    for c in self.list_child_channels(parent=l):
                        print(" |-%s" % c)

####################


def help_softwarechannel_listmanageablechannels(self):
    print('softwarechannel_listmanageablechannels: List all software channels')
    print('                                        manageable by current user')
    print('''usage: softwarechannel_listmanageablechannels [options]
options:
  -v verbose (display label and summary)''')


def do_softwarechannel_listmanageablechannels(self, args, doreturn=False):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    channels = self.client.channel.listManageableChannels(self.session)
    labels = [c.get('label') for c in channels]

    # filter the list if arguments were passed
    if args:
        labels = filter_results(labels, args, True)

    if doreturn:
        return labels
    elif labels:
        if options.verbose:
            for l in sorted(labels):
                details = \
                    self.client.channel.software.getDetails(self.session, l)

                print("%s : %s" % (l, details['summary']))
        else:
            for l in sorted(labels):
                print("%s" % l)

####################


def help_softwarechannel_listbasechannels(self):
    print('softwarechannel_listbasechannels: List all base software channels')
    print('''usage: softwarechannel_listbasechannels [options])
options:
  -v verbose (display label and summary)''')


def do_softwarechannel_listbasechannels(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    channels = self.list_base_channels()

    if channels:
        if (options.verbose):
            for c in sorted(channels):
                details = \
                    self.client.channel.software.getDetails(self.session, c)
                print("%s : %s" % (c, details['summary']))
        else:
            print('\n'.join(sorted(channels)))

####################


def help_softwarechannel_listchildchannels(self):
    print('softwarechannel_listchildchannels: List child software channels')
    print('usage:')
    print('softwarechannel_listchildchannels [options]')
    print('softwarechannel_listchildchannels : List all child channels')
    print('softwarechannel_listchildchannels CHANNEL : List children for a' +
          'specific base channel')
    print('options:\n -v verbose (display label and summary)')


def do_softwarechannel_listchildchannels(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)
    if not args:
        channels = self.list_child_channels()
    else:
        channels = self.list_child_channels(parent=args[0])

    if channels:
        if (options.verbose):
            for c in sorted(channels):
                details = \
                    self.client.channel.software.getDetails(self.session, c)
                print("%s : %s" % (c, details['summary']))
        else:
            print('\n'.join(sorted(channels)))

####################


def help_softwarechannel_listsystems(self):
    print('softwarechannel_listsystems: List all systems subscribed to')
    print('                             a software channel')
    print('usage: softwarechannel_listsystems CHANNEL')


def complete_softwarechannel_listsystems(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_listsystems(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listsystems()
        return

    channel = args[0]

    systems = \
        self.client.channel.software.listSubscribedSystems(self.session,
                                                           channel)

    systems = [s.get('name') for s in systems]

    if doreturn:
        return systems
    else:
        if systems:
            print('\n'.join(sorted(systems)))

####################


def help_softwarechannel_listpackages(self):
    print('softwarechannel_listpackages: List the most recent packages')
    print('                              available from a software channel')
    print('usage: softwarechannel_listpackages CHANNEL')


def complete_softwarechannel_listpackages(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)

    return []


def do_softwarechannel_listpackages(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listpackages()
        return

    channel = args[0]

    packages = self.client.channel.software.listLatestPackages(self.session,
                                                               channel)

    packages = build_package_names(packages)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(sorted(packages)))

####################


def help_softwarechannel_listallpackages(self):
    print('softwarechannel_listallpackages: List all packages in a channel')
    print('usage: softwarechannel_listallpackages CHANNEL')


def complete_softwarechannel_listallpackages(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)

    return []


def do_softwarechannel_listallpackages(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listallpackages()
        return

    channel = args[0]

    packages = self.client.channel.software.listAllPackages(self.session,
                                                            channel)

    packages = build_package_names(packages)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(sorted(packages)))

####################


def filter_latest_packages(pkglist):
    # This takes a list of package dicts, and returns a new list
    # which contains only the latest version, for each arch

    # First we generate a dict, indexed by a compound (tuple) key based on
    # arch and name, so we can store the latest version of each package
    # for each arch.  This approach avoids nested loops :)
    latest = {}
    for p in pkglist:
        tuplekey = p['name'], p['arch_label']
        if tuplekey not in latest:
            latest[tuplekey] = p
        else:
            # Already have this package, is p newer?
            if p == latest_pkg(p, latest[tuplekey]):
                latest[tuplekey] = p

    # Then return the dict items as a list
    return latest.values()


def help_softwarechannel_listlatestpackages(self):
    print('softwarechannel_listlatestpackages: List the newest version of all packages in a channel')
    print('usage: softwarechannel_listlatestpackages CHANNEL')


def complete_softwarechannel_listlatestpackages(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)

    return []


def do_softwarechannel_listlatestpackages(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listlatestpackages()
        return

    channel = args[0]

    allpackages = self.client.channel.software.listAllPackages(self.session,
                                                               channel)

    latestpackages = filter_latest_packages(allpackages)

    packages = build_package_names(latestpackages)

    if doreturn:
        return packages
    else:
        if packages:
            print('\n'.join(sorted(packages)))

####################


def help_softwarechannel_setdetails(self):
    print('softwarechannel_setdetails: Modify details of a software channel')
    print('''usage: softwarechannel_setdetails [options] <CHANNEL ...>)

options, at least one of which must be given:
  -n NAME
  -d DESCRIPTION
  -s SUMMARY
  -c CHECKSUM %s
  -m MAINTAINER_NAME
  -e MAINTAINER_EMAIL
  -p MAINTAINER_PHONE
  -u GPG_URL
  -i GPG_ID
  -f GPG_FINGERPRINT''' % CHECKSUM)


def complete_softwarechannel_setdetails(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_setdetails(self, args):
    # pylint: disable=R0911
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-s', '--summary')
    arg_parser.add_argument('-d', '--description')
    arg_parser.add_argument('-c', '--checksum')
    arg_parser.add_argument('-m', '--maintainer_name')
    arg_parser.add_argument('-e', '--maintainer_email')
    arg_parser.add_argument('-p', '--maintainer_phone')
    arg_parser.add_argument('-u', '--gpg_url')
    arg_parser.add_argument('-i', '--gpg_id')
    arg_parser.add_argument('-f', '--gpg_fingerprint')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_setdetails()
        return

    new_details = {}
    if options.name:
        new_details['name'] = options.name
    if options.summary:
        new_details['summary'] = options.summary
    if options.description:
        new_details['description'] = options.description
    if options.checksum:
        new_details['checksum_label'] = options.checksum
    if options.maintainer_name:
        new_details['maintainer_name'] = options.maintainer_name
    if options.maintainer_email:
        new_details['maintainer_email'] = options.maintainer_email
    if options.maintainer_phone:
        new_details['maintainer_phone'] = options.maintainer_phone
    if options.gpg_url:
        new_details['gpg_key_url'] = options.gpg_url
    if options.gpg_id:
        new_details['gpg_key_id'] = options.gpg_id
    if options.gpg_fingerprint:
        new_details['gpg_key_fp'] = options.gpg_fingerprint

    if not new_details:
        logging.error('At least one attribute to set must be given')
        return

    # allow globbing of software channel names
    channels = filter_results(self.do_softwarechannel_list('', True), args)
    if len(channels) < 1:
        logging.info('No channels matching that label')
        return

    # channel names must be unique, so if we are asked to set it,
    # take special precautions: first: check if we're doing this for
    # more than one channel (because if we do, first might succeed,
    # all the rest will obviously fail),
    # second: check if there's any other channel already using this name.
    # Not reusing softwarechannel_check_existing() here because it
    # also fails on same label.
    if new_details.get('name'):
        if len(channels) > 1:
            logging.error('Setting same name for more than ' + \
                          'one channel will fail')
            return
        logging.debug('checking other channels for name "%s"' %
                      new_details.get('name'))
        for c in self.list_base_channels() + self.list_child_channels():
            cd = self.client.channel.software.getDetails(self.session, c)
            if cd.get('name') == new_details.get('name'):
                logging.error('Name "%s" already in use by channel %s' %
                              (cd.get('name'), cd.get('label')))
                return

    # get confirmation
    print('Setting following attributes...')
    print('')
    if new_details.get('name'):
        print('Name:             %s' % new_details.get('name'))
    if new_details.get('summary'):
        print('Summary:          %s' % new_details.get('summary'))
    if new_details.get('description'):
        print('Description:      %s' % new_details.get('description'))
    if new_details.get('checksum_label'):
        print('Checksum:         %s' % new_details.get('checksum_label'))
    if new_details.get('maintainer_name'):
        print('Maintainer name:  %s' % new_details.get('maintainer_name'))
    if new_details.get('maintainer_email'):
        print('Maintainer email: %s' % new_details.get('maintainer_email'))
    if new_details.get('maintainer_phone'):
        print('Maintainer phone: %s' % new_details.get('maintainer_phone'))
    if new_details.get('gpg_key_id'):
        print('GPG Key:          %s' % new_details.get('gpg_key_id'))
    if new_details.get('gpg_key_fp'):
        print('GPG Fingerprint:  %s' % new_details.get('gpg_key_fp'))
    if new_details.get('gpg_key_url'):
        print('GPG URL:          %s' % new_details.get('gpg_key_url'))
    print('')
    print('... for the following channels:')
    print('\n'.join(channels))
    print('')
    if not self.user_confirm('Apply? [y/N]:'):
        return

    logging.debug('new channel details dictionary:')
    logging.debug(new_details)
    num_changed = 0
    for channel in channels:
        logging.debug('getting ID for channel %s' % channel)
        try:
            details = self.client.channel.software.getDetails(self.session,
                                                              channel)
        except xmlrpclib.Fault as e:
            logging.error('Could not get details for %s' % channel)
            logging.error(e)
            return
        channel_id = details.get('id')
        logging.debug('setting details for channel %s (%d)' % (channel,
                                                               channel_id))
        try:
            self.client.channel.software.setDetails(self.session,
                                                    channel_id,
                                                    new_details)
            num_changed += 1
        except xmlrpclib.Fault as e:
            logging.error('Error while setting details for %s' % channel)
            logging.error(e)
            return
    logging.info('Channels changed: %d' % num_changed)

####################


def help_softwarechannel_details(self):
    print('softwarechannel_details: Show the details of a software channel')
    print('usage: softwarechannel_details <CHANNEL ...>')


def complete_softwarechannel_details(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_details()
        return

    # allow globbing of software channel names
    channels = filter_results(self.do_softwarechannel_list('', True), args)

    add_separator = False

    for channel in channels:
        details = self.client.channel.software.getDetails(
            self.session, channel)

        systems = \
            self.client.channel.software.listSubscribedSystems(
                self.session, channel)

        trees = self.client.kickstart.tree.list(self.session,
                                                channel)

        packages = \
            self.client.channel.software.listAllPackages(
                self.session, channel)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Label:              %s' % details.get('label'))
        print('Name:               %s' % details.get('name'))
        print('Architecture:       %s' % details.get('arch_name'))
        print('Parent:             %s' % details.get('parent_channel_label'))
        print('Systems Subscribed: %s' % len(systems))
        print('Number of Packages: %i' % len(packages))

        if details.get('summary'):
            print('')
            print('Summary')
            print('-------')
            print('\n'.join(wrap(details.get('summary'))))

        if details.get('description'):
            print('')
            print('Description')
            print('-----------')
            print('\n'.join(wrap(details.get('description'))))

        print('')
        print('GPG Key:            %s' % details.get('gpg_key_id'))
        print('GPG Fingerprint:    %s' % details.get('gpg_key_fp'))
        print('GPG URL:            %s' % details.get('gpg_key_url'))
        print('GPG CHECK:          %s' % details.get('gpg_check'))

        if trees:
            print('')
            print('Kickstart Trees')
            print('---------------')
            for tree in trees:
                print(tree.get('label'))

        if details.get('contentSources'):
            print('')
            print('Repos')
            print('-----')
            for repo in details.get('contentSources'):
                print(repo.get('label'))

####################


def help_softwarechannel_listerrata(self):
    print('softwarechannel_listerrata: List the errata associated with a')
    print('                            software channel')
    print('usage: softwarechannel_listerrata <CHANNEL ...> [from=yyyymmdd [to=yyyymmdd]]')


def complete_softwarechannel_listerrata(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_listerrata(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listerrata()
        return

    begin_date = None
    end_date = None

    # iterate over args and alter a copy of it (channels)
    channels = args[:]
    for arg in args:
        if arg[:5] == 'from=':
            begin_date = arg[5:]
            channels.remove(arg)
        elif arg[:3] == 'to=':
            end_date = arg[3:]
            channels.remove(arg)

    add_separator = False

    for channel in sorted(channels):
        if len(channels) > 1:
            print('Channel: %s' % channel)
            print('')

        if begin_date and end_date:
            errata = self.client.channel.software.listErrata(self.session,
                                                             channel, parse_time_input(begin_date),
                                                             parse_time_input(end_date))
        elif begin_date:
            errata = self.client.channel.software.listErrata(self.session,
                                                             channel, parse_time_input(begin_date))
        else:
            errata = self.client.channel.software.listErrata(self.session,
                                                             channel)

        print_errata_list(errata)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

####################

def help_softwarechannel_listarches(self):
    print("softwarechannel_listarches: lists the potential software")
    print("                            channel architectures that can be created")
    print("usage: softwarechannel_listarches")
    print("options:")
    print("    -v verbose (display label and name)")

def do_softwarechannel_listarches(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    arches = self.client.channel.software.listArches(self.session)

    for arch in sorted(arches):
        if (options.verbose):
            print("%s (%s)" % (arch["label"], arch["name"]))
        else:
            print("%s" % arch["label"])

####################

def help_softwarechannel_delete(self):
    print('softwarechannel_delete: Delete a software channel')
    print('usage: softwarechannel_delete <CHANNEL ...>')


def complete_softwarechannel_delete(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_delete()
        return

    channels = args

    # find all matching channels
    to_delete = filter_results(
        self.do_softwarechannel_list('', True), channels)

    if not to_delete:
        return

    print('Channels')
    print('--------')
    print('\n'.join(sorted(to_delete)))

    if not self.user_confirm('Delete these channels [y/N]:'):
        return

    # delete child channels first to avoid errors
    parents = []
    children = []

    all_channels = self.client.channel.listSoftwareChannels(self.session)

    for channel in all_channels:
        if channel.get('label') in to_delete:
            if channel.get('parent_label'):
                children.append(channel.get('label'))
            else:
                parents.append(channel.get('label'))

    for channel in children + parents:
        self.client.channel.software.delete(self.session, channel)
####################
def help_softwarechannel_update(self):
    print('softwarechannel_update: Update a software channel')
    print('''usage: softwarechannel_update LABEL(To identify the channel) [options]
options:
  -l LABEL(Required)
  -n NAME
  -s SUMMARY
  -d DESCRIPTION
  -c CHECKSUM %s
  -u GPG-URL
  -i GPG-ID
  -f GPG-FINGERPRINT
  -g DISABLE-GPG-CHECK''' % CHECKSUM)


def do_softwarechannel_update(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-l', '--label')
    arg_parser.add_argument('-s', '--summary')
    arg_parser.add_argument('-d', '--description')
    arg_parser.add_argument('-c', '--checksum')
    arg_parser.add_argument('-u', '--gpg_url')
    arg_parser.add_argument('-i', '--gpg_id')
    arg_parser.add_argument('-f', '--gpg_fingerprint')
    arg_parser.add_argument('-g', '--disable_gpg_check')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
       options.label = prompt_user('Channel Label:', noblank=True)

       print('')
       print('New Name (blank to keep unchanged)')
       print('------------')
       print('')
       options.name = prompt_user('Name:')

       print('')
       print('New Summary (blank to keep unchanged)')
       print('------------')
       print('')
       options.summary = prompt_user('Summary:')

       print('')
       print('New Description (blank to keep unchanged)')
       print('------------')
       print('')
       options.description = prompt_user('Description:')

       print('')
       print('New Checksum type (blank to keep unchanged)')
       print('------------')
       print('\n'.join(sorted(self.CHECKSUM)))
       print('')
       options.checksum = prompt_user('Select:')

       print('')
       print('New GPG URL (blank to keep unchanged)')
       print('------------')
       print('')
       options.gpg_url = prompt_user('GPG URL:')

       print('')
       print('New GPG ID (blank to keep unchanged)')
       print('------------')
       print('')
       options.gpg_id = prompt_user('GPG ID:')

       print('')
       print('New GPG Fingerprint (blank to keep unchanged)')
       print('------------')
       print('')
       options.gpg_fingerprint = prompt_user('GPG Fingerprint:')

       print('')
       print('Disable GPG CHECK (blank to keep unchanged)')
       print('------------')
       print('')
       options.disable_gpg_check = prompt_user('Disable GPG Check [yes/no]? ')

    if not options.label:
        logging.error('A channel label is required to identify the channel')
        return
    if options.disable_gpg_check and options.disable_gpg_check not in ('yes','no'):
        logging.error('Only [yes/no] values are acceptable for --disable_gpg_check')
        return
    details = {}
    if options.name:
        details['name'] = options.name

    if options.summary:
        details['summary'] = options.summary

    if options.description:
        details['description'] = options.description

    if options.checksum:
        details['checksum_label'] = options.checksum

    if options.gpg_id:
        details['gpg_key_id'] = options.gpg_id

    if options.gpg_url:
        details['gpg_key_url'] = options.gpg_url

    if options.gpg_fingerprint:
        details['gpg_key_fp'] = options.gpg_fingerprint

    if options.disable_gpg_check:
        details['gpg_check'] = str(not options.disable_gpg_check.lower() == "yes")

    self.client.channel.software.setDetails(self.session, options.label, details)
####################


def help_softwarechannel_create(self):
    print('softwarechannel_create: Create a software channel')
    print('''usage: softwarechannel_create [options])

options:
  -n NAME
  -l LABEL
  -s SUMMARY
  -p PARENT_CHANNEL
  -a ARCHITECTURE
  -c CHECKSUM %s
  -u GPG_URL
  -i GPG_ID
  -f GPG_FINGERPRINT
  -g DISABLE_GPG_CHECK''' % CHECKSUM)


def do_softwarechannel_create(self, args):
    arches = self.client.channel.software.listArches(self.session)
    self.ARCH_LABELS = [x["label"].replace("channel-","") for x in arches]
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-l', '--label')
    arg_parser.add_argument('-s', '--summary')
    arg_parser.add_argument('-p', '--parent-channel')
    arg_parser.add_argument('-a', '--arch')
    arg_parser.add_argument('-c', '--checksum')
    arg_parser.add_argument('-u', '--gpg_url')
    arg_parser.add_argument('-i', '--gpg_id')
    arg_parser.add_argument('-f', '--gpg_fingerprint')
    arg_parser.add_argument('-g', '--disable_gpg_check', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.name = prompt_user('Channel Name:', noblank=True)
        options.label = prompt_user('Channel Label:', noblank=True)
        options.summary = prompt_user('Channel Summary:', noblank=True)

        print('Base Channels')
        print('-------------')
        print('\n'.join(sorted(self.list_base_channels())))
        print('')

        options.parent_channel = \
            prompt_user('Select Parent [blank to create a base channel]:')

        print('')
        print('Architecture')
        print('------------')
        print('\n'.join(sorted(self.ARCH_LABELS)))
        print('')
        options.arch = prompt_user('Select:')

        print('')
        print('Checksum type')
        print('------------')
        print('\n'.join(sorted(self.CHECKSUM)))
        print('')
        options.checksum = prompt_user('Select:')

        print('')
        print('GPG URL')
        print('------------')
        print('')
        options.gpg_url = prompt_user('GPG URL:')

        print('')
        print('GPG ID')
        print('------------')
        print('')
        options.gpg_id = prompt_user('GPG ID:')

        print('')
        print('GPG Fingerprint')
        print('---------------')
        print('')
        options.gpg_fingerprint = prompt_user('GPG Fingerprint:')

        print('')
        print('GPG CHECK')
        print('---------------')
        print('')
        options.disable_gpg_check = self.user_confirm('Disable GPG Check [y/N]? ',
                                nospacer=True, ignore_yes=True)

    if validate_required_data(options):
        set_default_data(options)
        gpgData = get_gpg_data(options)
        self.client.channel.software.create(self.session, options.label, options.name, options.summary,
                                            'channel-%s' % options.arch, options.parent_channel,
                                            options.checksum, gpgData, not options.disable_gpg_check
                                       )

####################


def get_gpg_data(options):
    gpgData = {}

    if options.gpg_url:
        gpgData['url'] = options.gpg_url

    if options.gpg_id:
        gpgData['id'] = options.gpg_id

    if options.gpg_fingerprint:
        gpgData['fingerprint'] = options.gpg_fingerprint

    return gpgData
####################


def set_default_data(options):
    if not options.arch:
        options.arch = 'x86_64'

    if not options.checksum:
        options.checksum = 'sha256'

    if not options.parent_channel:
        options.parent_channel = ''

    # Summary is a required field,
    # but we don't want to break the interface
    # then if it is not provided it is set to the 'name' value
    if not options.summary:
        options.summary = options.name
####################


def validate_required_data(options):
    if not options.name:
        logging.error('A channel name is required')
        return False

    if not options.label:
        logging.error('A channel label is required')
        return False

    return True
######################


def softwarechannel_check_existing(self, name, label):
    # Catch label or name which already exists, duplicate label throws a
    # descriptive xmlrpc error, but duplicate name results in ISE
    for c in self.list_base_channels() + self.list_child_channels():
        cd = self.client.channel.software.getDetails(self.session, c)
        if cd['name'] == name:
            logging.error("Name %s already in use by channel %s" %
                          (name, cd['label']))
            return True
        if cd['label'] == label:
            logging.error("Label %s already in use by channel %s" %
                          (label, cd['label']))
            return True
    return False
########################


def help_softwarechannel_clone(self):
    print('softwarechannel_clone: Clone a software channel')
    print('''usage: softwarechannel_clone [options])

options:
  -s SOURCE_CHANNEL
  -n NAME
  -l LABEL
  -p PARENT_CHANNEL
  --gpg-copy/-g (copy SOURCE_CHANNEL GPG details)
  --gpg-url GPG_URL
  --gpg-id GPG_ID
  --gpg-fingerprint(GPG_FINGERPRINT)
  --disable-gpg-check DISABLE_GPG_CHECK
  -o do not clone any patches
  --regex/-x "s/foo/bar" : Optional regex replacement,
        replaces foo with bar in the clone name and label''')


def do_softwarechannel_clone(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-l', '--label')
    arg_parser.add_argument('-s', '--source-channel')
    arg_parser.add_argument('-p', '--parent-channel')
    arg_parser.add_argument('-x', '--regex')
    arg_parser.add_argument('-o', '--original-state', action='store_true')
    arg_parser.add_argument('-g', '--gpg-copy', action='store_true')
    arg_parser.add_argument('--gpg-url')
    arg_parser.add_argument('--gpg-id')
    arg_parser.add_argument('--gpg-fingerprint')
    arg_parser.add_argument('--disable-gpg-check')


    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        print('Source Channels:')
        print('\n'.join(sorted(self.list_base_channels())))
        print('\n'.join(sorted(self.list_child_channels())))

        options.source_channel = prompt_user('Select source channel:',noblank=True)
        options.name = prompt_user('Channel Name:', noblank=True)
        options.label = prompt_user('Channel Label:', noblank=True)

        print('Base Channels:')
        print('\n'.join(sorted(self.list_base_channels())))
        print('')

        options.parent_channel = \
            prompt_user('Select Parent [blank to create a base channel]:')

        options.gpg_copy = \
            self.user_confirm('Copy source channel GPG details? [y/N]:',
                              ignore_yes=True)
        if not options.gpg_copy:
            options.gpg_url = prompt_user('GPG URL:')
            options.gpg_id = prompt_user('GPG ID:')
            options.gpg_fingerprint = prompt_user('GPG Fingerprint:')
            options.disable_gpg_check = prompt_user('Disable GPG Check [yes/no]? ')

        options.original_state = \
            self.user_confirm('Original State (No Errata) [y/N]:',
                              ignore_yes=True)
    else:
        if not options.source_channel:
            logging.error('A source channel is required')
            return

        if not options.name and not options.regex:
            logging.error('A channel name is required')
            return

        if not options.label and not options.regex:
            logging.error('A channel label is required')
            return

        if not options.original_state:
            options.original_state = False

        if options.regex:
            newvalues =do_regx_replacement(self,options.source_channel, options)
            options.label = newvalues['label']
            if not options.name:
                options.name = newvalues['name']

    if options.disable_gpg_check and options.disable_gpg_check not in ('yes', 'no'):
        logging.error('Only [yes/no] values are acceptable for --disable-gpg-check')
        return
    if self.softwarechannel_check_existing(options.name, options.label):
        return

    details = {'name': options.name, 'label': options.label}
    if options.parent_channel:
        details['parent_label'] = options.parent_channel

    clone_channel(self,options.source_channel, options, details)


####################
def help_softwarechannel_clonetree(self):
    print('softwarechannel_clonetree: Clone a software channel and its child channels')
    print('''usage: softwarechannel_clonetree [options])
             e.g    softwarechannel_clonetree foobasechannel -p "my_"
                    softwarechannel_clonetree foobasechannel -x "s/foo/bar"
                    softwarechannel_clonetree foobasechannel -x "s/^/my_"

options:
  -s/--source-channel SOURCE_CHANNEL
  -p/--prefix PREFIX (is prepended to the label and name of all channels)
  --gpg-copy/-g (copy GPG details for correspondoing source channel))
  --gpg-url GPG_URL (applied to all channels)
  --gpg-id GPG_ID (applied to all channels)
  --gpg-fingerprint(GPG_FINGERPRINT (applied to all channels))
  --disable-gpg-check DISABLE_GPG_CHECK(applied to all channels)
  -o do not clone any errata
  --regex/-x "s/foo/bar" : Optional regex replacement,
        replaces foo with bar in the clone name, label and description''')


def do_softwarechannel_clonetree(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--source-channel')
    arg_parser.add_argument('-p', '--prefix')
    arg_parser.add_argument('-x', '--regex')
    arg_parser.add_argument('-o', '--original-state', action='store_true')
    arg_parser.add_argument('-g', '--gpg-copy', action='store_true')
    arg_parser.add_argument('--gpg-url')
    arg_parser.add_argument('--gpg-id')
    arg_parser.add_argument('--gpg-fingerprint')
    arg_parser.add_argument('--disable-gpg-check')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        print('Source Channels:')
        print('\n'.join(sorted(self.list_base_channels())))

        options.source_channel = prompt_user('Select source channel:',noblank=True)
        options.prefix = prompt_user('Prefix:', noblank=True)
        options.gpg_copy = \
            self.user_confirm('Copy source channel GPG details? [y/N]:', ignore_yes=True)
        if not options.gpg_copy:
            options.gpg_url = prompt_user('GPG URL:')
            options.gpg_id = prompt_user('GPG ID:')
            options.gpg_fingerprint = prompt_user('GPG Fingerprint:')
            options.disable_gpg_check = prompt_user('Disable GPG Check [yes/no]? ')

        options.original_state = \
            self.user_confirm('Original State (No Errata) [y/N]:', ignore_yes=True)
    else:
        if not options.source_channel:
            logging.error('A source channel is required')
            return

        if not options.prefix and not options.regex:
            logging.error('A prefix or regex is required')
            return

        if not options.original_state:
            options.original_state = False

    if options.disable_gpg_check and options.disable_gpg_check not in ('yes', 'no'):
        logging.error('Only [yes/no] values are acceptable for --disable-gpg-check')
        return

    channels = [options.source_channel]
    if not options.source_channel in self.list_base_channels():
        logging.error("Channel does not exist or is not a base channel!")
        self.help_softwarechannel_clonetree()
        return
    logging.debug("--child mode specified, finding children of %s\n" % options.source_channel)
    children = self.list_child_channels(parent=options.source_channel)
    logging.debug("Found children %s\n" % children)
    for c in children:
        channels.append(c)

    logging.debug("channels=%s" % channels)
    parent_channel = None
    for ch in channels:
        logging.debug("Cloning %s" % ch)
        label = None
        name = None
        if options.regex:
            # Expect option to be formatted like a sed-replacement, s/foo/bar
            newvalues = do_regx_replacement(self, ch, options)
            label = newvalues['label']
            name = newvalues['name']

        elif options.prefix:
            srcdetails = self.client.channel.software.getDetails(self.session, ch)
            label = options.prefix + srcdetails['label']
            name = options.prefix + srcdetails['name']
        else:
            # Shouldn't ever get here due to earlier checks
            logging.error("called without prefix or regex option!")
            return

        # Catch label or name which already exists
        if self.softwarechannel_check_existing(name, label):
            return

        details = {'name': name, 'label': label}
        if parent_channel:
            details['parent_label'] = parent_channel
        clone_channel(self, ch, options, details)

        # If this is the first call we are on the base-channel clone and we
        # need to set parent_channel to the new cloned base-channel label
        if not parent_channel:
            parent_channel = details['label']

###################


def clone_channel(self, channel, options, details) :

    if options.gpg_copy:
        srcdetails = self.client.channel.software.getDetails(self.session, channel)
        copy_gpg_values_from_source(details, srcdetails);

    if options.gpg_id:
        details['gpg_id'] = options.gpg_id

    if options.gpg_url:
        details['gpg_url'] = options.gpg_url

    if options.gpg_fingerprint:
        details['gpg_fingerprint'] = options.gpg_fingerprint

    if options.disable_gpg_check:
        details['gpg_check'] = str(not options.disable_gpg_check.lower() == "yes")

    apiversion = self.client.api.getVersion()
    if apiversion and int(apiversion) <= 19:
        details['summary'] = details['name']

    # remove empty strings from the structure
    to_remove = []
    for key in details:
        if details[key] == '':
            to_remove.append(key)

    for key in to_remove:
        del details[key]
    logging.info("Cloning %s as %s" % (channel, details['label']))
    self.client.channel.software.clone(self.session,
                                       channel,
                                       details,
                                       options.original_state)

###################


def copy_gpg_values_from_source(details, srcdetails):
    if srcdetails['gpg_key_url']:
        details['gpg_key_url'] = srcdetails['gpg_key_url']
        logging.debug("copying gpg_key_url=%s" % srcdetails['gpg_key_url'])
    if srcdetails['gpg_key_id']:
        details['gpg_key_id'] = srcdetails['gpg_key_id']
        logging.debug("copying gpg_key_id=%s" % srcdetails['gpg_key_id'])
    if srcdetails['gpg_key_fp']:
        details['gpg_key_fp'] = srcdetails['gpg_key_fp']
        logging.debug("copying gpg_key_fp=%s" % srcdetails['gpg_key_fp'])
    if srcdetails['gpg_check']:
        details['gpg_check'] = str(srcdetails['gpg_check'])
        logging.debug("copying gpg_check=%s" % srcdetails['gpg_check'])

####################


# If the -x/--regex option is passed, do a sed-style replacement over
# the name, label and description. from the source channel to create
# the name, label and description for the clone channel.
# This makes it easier to clone based on a known naming convention
def do_regx_replacement(self,channel, options):
    newvalues ={}
    # Expect option to be formatted like a sed-replacement, s/foo/bar
    findstr = options.regex.split("/")[1]
    replacestr = options.regex.split("/")[2]
    logging.debug("--regex selected with %s, replacing %s with %s" %(options.regex, findstr, replacestr))
    srcdetails = self.client.channel.software.getDetails(self.session, channel)

    newvalues['name'] = re.sub(findstr, replacestr, srcdetails['name'])
    newvalues['label'] = re.sub(findstr, replacestr, channel)
    logging.debug("regex mode : %s %s %s" % (options.source_channel,  newvalues['name'], newvalues['label']))

    return newvalues;

####################


def help_softwarechannel_addpackages(self):
    print('softwarechannel_addpackages: Add packages to a software channel')
    print('usage: softwarechannel_addpackages CHANNEL <PACKAGE ...>')


def complete_softwarechannel_addpackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(True), text)


def do_softwarechannel_addpackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_softwarechannel_addpackages()
        return

    # Take the first argument as the channel and validate it
    channel = args.pop(0)
    if not channel in self.do_softwarechannel_list('', True):
        logging.error("%s is not a valid channel" % channel)
        self.help_softwarechannel_addpackages()
        return

    # expand the arguments to search for packages
    package_names = []
    for item in args:
        package_names.extend(self.do_package_search(item, True))

    # get the package IDs from the names
    package_ids = []
    for package in package_names:
        package_ids += self.get_package_id(package)

    if not package_ids:
        logging.warning('No packages to add')
        return

    print('Packages')
    print('--------')
    print('\n'.join(sorted(package_names)))

    if not self.user_confirm('Add these packages [y/N]:'):
        return

    self.client.channel.software.addPackages(self.session,
                                             channel,
                                             package_ids)

####################

def help_softwarechannel_mergepackages(self):
    print('softwarechannel_mergepackages: Merge packages from one software channel into another')
    print('usage: softwarechannel_mergepackages SOURCE_CHANNEL TARGET_CHANNEL')

def complete_softwarechannel_mergepackages(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    if len(parts) == 3:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)

def do_softwarechannel_mergepackages(self, args):
    '''
    Does the same thing as do_softwarechannel_sync
    with the exception of deleting packages from
    the target channel.
    '''
    self.do_softwarechannel_sync(args, deleteFromTarget=False)

####################


def help_softwarechannel_removeerrata(self):
    print('softwarechannel_removeerrata: Remove patches from a ' +
          'software channel')
    print('usage: softwarechannel_removeerrata CHANNEL <ERRATA:search:XXX ...>')


def complete_softwarechannel_removeerrata(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    elif len(parts) > 2:
        return self.tab_complete_errata(text)


def do_softwarechannel_removeerrata(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_removeerrata()
        return

    channel = args[0]
    errata_wanted = self.expand_errata(args[1:])

    logging.debug('Retrieving the list of patches from source channel')
    channel_errata = self.client.channel.software.listErrata(self.session,
                                                             channel)

    errata = filter_results([e.get('advisory_name') for e in channel_errata],
                            errata_wanted)

    # keep the details for our matching errata so we can use them later
    errata_details = []
    for erratum in channel_errata:
        if erratum.get('advisory_name') in errata:
            errata_details.append(erratum)

    # get the packages that resolve these errata so we can add them
    # to the channel afterwards
    package_ids = []
    for erratum in errata:
        logging.debug('Retrieving packages for patch %s' % erratum)

        # get the packages affected by this errata
        packages = self.client.errata.listPackages(self.session, erratum)

        # only add packages that exist in the source channel
        for package in packages:
            if channel in package.get('providing_channels'):
                package_ids.append(package.get('id'))

    if not errata_details:
        logging.warning('No patches to remove')
        return

    print_errata_list(errata_details)

    print('')
    print('Packages')
    print('--------')
    print('\n'.join(sorted([ self.get_package_name(p) for p in package_ids if self.get_package_name(p) ])))

    print('')
    print('Total Errata:   %s' % str(len(errata)).rjust(3))
    print('Total Packages: %s' % str(len(package_ids)).rjust(3))

    if not self.user_confirm('Remove these patches [y/N]:'):
        return

    # remove the errata and the packages they brought in
    self.client.channel.software.removeErrata(self.session,
                                              channel,
                                              errata,
                                              True)

####################


def help_softwarechannel_removepackages(self):
    print('softwarechannel_removepackages: Remove packages from a ' +
          'software channel')
    print('usage: softwarechannel_removepackages CHANNEL <PACKAGE ...>')


def complete_softwarechannel_removepackages(self, text, line, beg,
                                            end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    elif len(parts) > 2:
        # only tab complete packages in the channel
        package_names = []
        try:
            packages = \
                self.client.channel.software.listAllPackages(self.session,
                                                             parts[1])

            package_names = build_package_names(packages)
        except xmlrpclib.Fault:
            package_names = []

        return tab_completer(package_names, text)


def do_softwarechannel_removepackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_removepackages()
        return

    channel = args.pop(0)
    package_list = args

    # get all the packages in the channel
    packages = \
        self.client.channel.software.listAllPackages(self.session,
                                                     channel)

    # build full names for those packages
    installed_packages = build_package_names(packages)

    # find matching packages that are in the channel
    package_names = filter_results(installed_packages, package_list)

    # get the package IDs from the names
    package_ids = []
    for package in package_names:
        package_ids += self.get_package_id(package)

    if not package_ids:
        logging.warning('No packages to remove')
        return

    print('Packages')
    print('--------')
    print('\n'.join(sorted(package_names)))

    if not self.user_confirm('Remove these packages [y/N]:'):
        return

    self.client.channel.software.removePackages(self.session,
                                                channel,
                                                package_ids)

####################


def help_softwarechannel_adderratabydate(self):
    print('softwarechannel_adderratabydate: Add errata from one channel ' +
          'into another channel based on a date range')
    print('usage: softwarechannel_adderratabydate [options] SOURCE DEST BEGINDATE ENDDATE')
    print('Date format : YYYYMMDD')
    print('Options:')
    print('        -p/--publish : Publish errata to the channel (don\'t clone)')


def complete_softwarechannel_adderratabydate(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) <= 3:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)


def do_softwarechannel_adderratabydate(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-p', '--publish', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) != 4:
        self.help_softwarechannel_adderratabydate()
        return

    source_channel = args[0]
    dest_channel = args[1]
    begin_date = args[2]
    end_date = args[3]

    if not re.match(r'\d{8}', begin_date):
        logging.error('%s is an invalid date' % begin_date)
        self.help_softwarechannel_adderratabydate()
        return

    if not re.match(r'\d{8}', end_date):
        logging.error('%s is an invalid date' % end_date)
        self.help_softwarechannel_adderratabydate()
        return

    # get the errata that are in the given date range
    logging.debug('Retrieving list of patches from source channel')
    errata = \
        self.client.channel.software.listErrata(self.session,
                                                source_channel,
                                                parse_time_input(begin_date),
                                                parse_time_input(end_date))

    if not errata:
        logging.warning('No patches found between the given dates')
        return

    if options.publish:
        # Just publish the errata one-by-one, rather than calling
        # do_softwarechannel_adderrata which clones the errata
        for e in errata:
            logging.info("Publishing errata %s to %s" %
                         (e.get('advisory_name'), dest_channel))
            self.client.errata.publish(self.session, e.get('advisory_name'),
                                       [dest_channel])
    else:
        # call adderrata with the list of errata from the date range
        # this clones the errata and adds it to the channel
        return self.do_softwarechannel_adderrata('%s %s %s' % (
            source_channel,
            dest_channel,
            ' '.join([e.get('advisory_name') for e in errata])))

####################


def help_softwarechannel_listerratabydate(self):
    print('softwarechannel_listerratabydate: list errata from channel' +
          'based on a date range')
    print('usage: softwarechannel_listerratabydate CHANNEL BEGINDATE ENDDATE')
    print('Date format : YYYYMMDD')


def complete_softwarechannel_listerratabydate(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) <= 3:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)


def do_softwarechannel_listerratabydate(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 3:
        self.help_softwarechannel_listerratabydate()
        return

    channel = args[0]
    begin_date = args[1]
    end_date = args[2]

    if not re.match(r'\d{8}', begin_date):
        logging.error('%s is an invalid date' % begin_date)
        self.help_softwarechannel_listerratabydate()
        return

    if not re.match(r'\d{8}', end_date):
        logging.error('%s is an invalid date' % end_date)
        self.help_softwarechannel_listerratabydate()
        return

    # get the errata that are in the given date range
    logging.debug('Retrieving list of errata from channel %s' % channel)
    errata = \
        self.client.channel.software.listErrata(self.session,
                                                channel,
                                                parse_time_input(begin_date),
                                                parse_time_input(end_date))

    if not errata:
        logging.warning('No errata found between the given dates')
        return

    print_errata_list(errata)

####################


def help_softwarechannel_adderrata(self):
    print('softwarechannel_adderrata: Add patches from one channel ' +
          'into another channel')
    print('usage: softwarechannel_adderrata SOURCE DEST <ERRATA|search:XXX ...>')
    print('Options:')
    print('    -q/--quick : Don\'t display list of packages (slightly faster)')
    print('    -s/--skip :  Skip errata which appear to exist already in DEST')


def complete_softwarechannel_adderrata(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) <= 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    elif len(parts) > 3:
        return self.tab_complete_errata(text)


def do_softwarechannel_adderrata(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-q', '--quick', action='store_true')
    arg_parser.add_argument('-s', '--skip', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_softwarechannel_adderrata()
        return

    allchannels = self.do_softwarechannel_list('', True)
    source_channel = args[0]
    if not source_channel in allchannels:
        logging.error("source channel %s does not exist!" % source_channel)
        self.help_softwarechannel_adderrata()
        return
    dest_channel = args[1]
    if not dest_channel in allchannels:
        logging.error("dest channel %s does not exist!" % dest_channel)
        self.help_softwarechannel_adderrata()
        return
    errata_wanted = self.expand_errata(args[2:])

    logging.debug('Retrieving the list of patches from source channel')
    source_errata = self.client.channel.software.listErrata(self.session,
                                                            source_channel)
    dest_errata = self.client.channel.software.listErrata(self.session,
                                                          dest_channel)

    errata = filter_results([e.get('advisory_name') for e in source_errata],
                            errata_wanted)
    logging.debug("errata = %s" % errata)
    if options.skip:
        # We just match the NNNN:MMMM of the XXXX-NNNN:MMMM as the
        # source errata will be RH[BES]A and the DEST errata will be CLA
        dest_errata_suffix = [x.get('advisory_name').split("-")[1]
                              for x in dest_errata]
        logging.debug("dest_errata_suffix = %s" % dest_errata_suffix)
        toremove = []
        for e in errata:
            if e.split("-")[1] in dest_errata_suffix:
                logging.debug("Skipping errata %s as it seems to be in %s" %
                              (e, dest_channel))
                toremove.append(e)
        for e in toremove:
            logging.debug("Removing %s from errata to be added" % e)
            errata.remove(e)
        logging.debug("skip-mode : reduced errata = %s" % errata)

    # keep the details for our matching errata so we can use them later
    errata_details = []
    for erratum in source_errata:
        if erratum.get('advisory_name') in errata:
            errata_details.append(erratum)

    if not options.quick:
        # get the packages that resolve these errata so we can add them
        # to the channel afterwards
        package_ids = []
        for erratum in errata:
            logging.debug('Retrieving packages for patch %s' % erratum)

            # get the packages affected by this errata
            packages = self.client.errata.listPackages(self.session, erratum)

            # only add packages that exist in the source channel
            for package in packages:
                if source_channel in package.get('providing_channels'):
                    package_ids.append(package.get('id'))

    if not errata:
        logging.warning('No patch to add')
        return

    # show the user which errata will be added
    print_errata_list(errata_details)

    if not options.quick:
        print('')
        print('Packages')
        print('--------')
        print('\n'.join(
            sorted([self.get_package_name(p) for p in package_ids])))

        print('')
    print('Total Errata:   %s' % str(len(errata)).rjust(3))

    if not options.quick:
        print('Total Packages: %s' % str(len(package_ids)).rjust(3))

    if not self.user_confirm('Add these patches [y/N]:'):
        return

    # clone each erratum individually because the process is slow and it can
    # lead to timeouts on the server
    for erratum in errata:
        logging.debug('Cloning %s' % erratum)
        if self.check_api_version('10.11'):
            # This call is poorly documented, but it stops errata.clone
            # pushing EL6 packages into EL5 channels when the errata
            # package list contains both versions, ref bz678721
            self.client.errata.cloneAsOriginal(self.session, dest_channel,
                                               [erratum])
        else:
            logging.warning("Using the old errata.clone function")
            logging.warning("If you have base channels for multiple OS" +
                            " versions, check no unexpected packages have been added")
            self.client.errata.clone(self.session, dest_channel, [erratum])

    # regenerate the errata cache since we just cloned errata
    self.generate_errata_cache(True)

####################


def help_softwarechannel_getorgaccess(self):
    print('softwarechannel_getorgaccess: Get the org-access for the software channel')
    print('usage: softwarechannel_getorgaccess [CHANNEL ...]')


def complete_softwarechannel_getorgaccess(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_listmanageablechannels('', doreturn=True),
                         text)


def do_softwarechannel_getorgaccess(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    # If no args are passed, we dump the org access for all channels
    if not args:
        channels = self.do_softwarechannel_listmanageablechannels('', doreturn=True)
    else:
        # allow globbing of software channel names
        channels = filter_results(
            self.do_softwarechannel_listmanageablechannels('', doreturn=True), args)

    for channel in channels:
        logging.debug("Getting org-access for channel %s" % channel)
        sharing = self.client.channel.access.getOrgSharing(
            self.session, channel)
        print("%s : %s" % (channel, sharing))
        if sharing == 'protected':
            # for protected channels list each organization's access status
            channel_orgs = self.client.channel.org.list(self.session, channel)
            for org in channel_orgs:
                print("\t%s : %s" % (org["org_name"], org["access_enabled"]))

####################


def help_softwarechannel_setorgaccess(self):
    print('softwarechannel_setorgaccess: Set the org-access for the software channel')
    print('''usage : softwarechannel_setorgaccess <CHANNEL> [options])
-d,--disable : disable org access (private, no org sharing)
-e,--enable : enable org access (public access to all trusted orgs)
-p,--protected ORG : protected org access for ORG only (multiple instances of -p ORG are allowed)''')


def complete_softwarechannel_setorgaccess(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_listmanageablechannels('', doreturn=True),
                         text)


def do_softwarechannel_setorgaccess(self, args, options=None):
    if not args:
        self.help_softwarechannel_setorgaccess()
        return
    if not options:
        arg_parser = get_argument_parser()
        arg_parser.add_argument('-e', '--enable', action='store_true')
        arg_parser.add_argument('-d', '--disable', action='store_true')
        arg_parser.add_argument('-p', '--protected', action='append')

        (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_setorgaccess()
        return

    # allow globbing of software channel names
    channels = filter_results(self.do_softwarechannel_listmanageablechannels('', doreturn=True),
                              args)

    # get the list of trusted organizations when we are dealing with protected channels
    if (options.protected):
        org_trust_list = self.client.org.trusts.listOrgs(self.session)

    for channel in channels:
        # If they just specify a channel and --enable/--disable
        # this implies public/private access
        if (options.enable):
            logging.info(
                "Making org sharing public for channel : %s " % channel)
            self.client.channel.access.setOrgSharing(
                self.session, channel, 'public')
        elif (options.disable):
            logging.info(
                "Making org sharing private for channel : %s " % channel)
            self.client.channel.access.setOrgSharing(
                self.session, channel, 'private')
        elif (options.protected):
            logging.info(
                "Making org sharing protected for channel : %s " % channel)
            self.client.channel.access.setOrgSharing(
                self.session, channel, 'protected')
            for org in org_trust_list:
                if org["org_name"] in options.protected:
                    logging.info(
                        "Enabling %s access for channel : %s " % (org["org_name"],channel))
                    self.client.channel.org.enableAccess(
                        self.session, channel, org["org_id"])
                else:
                    logging.info(
                        "Disabling %s access for channel : %s " % (org["org_name"],channel))
                    self.client.channel.org.disableAccess(
                        self.session, channel, org["org_id"])
        else:
            self.help_softwarechannel_setorgaccess()
            return

####################


def help_softwarechannel_getorgaccesstree(self):
    print('softwarechannel_getorgaccesstree: Get the org-access for a software base channel and its children')
    print('usage: softwarechannel_getorgaccesstree [CHANNEL]')

def complete_softwarechannel_getorgaccesstree(self, text, line, beg, end):
    return tab_completer(self.list_base_channels(), text)

def do_softwarechannel_getorgaccesstree(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    # If no args are passed, we dump the org access for all base channels
    if not args:
        channels = self.list_base_channels()
    else:
        # allow globbing of software channel names
        channels = filter_results(self.list_base_channels(), args)

    if not channels:
        logging.error("Channel does not exist or is not a base channel!")
        self.help_softwarechannel_getorgaccesstree()
        return

    for channel in channels:
        do_softwarechannel_getorgaccess(self, channel)
        for child in self.list_child_channels(parent=channel):
            do_softwarechannel_getorgaccess(self, child)

####################


def help_softwarechannel_setorgaccesstree(self):
    print('softwarechannel_setorgaccesstree: set the org-access for a software base channel and its children')
    print('''usage: softwarechannel_setorgaccesstree <CHANNEL> [options])
-d,--disable : disable org access (private, no org sharing)
-e,--enable : enable org access (public access to all trusted orgs)
-p,--protected ORG : protected org access for ORG only (multiple instances of -p ORG are allowed)''')

def complete_softwarechannel_setorgaccesstree(self, text, line, beg, end):
    return tab_completer(self.list_base_channels(), text)

def do_softwarechannel_setorgaccesstree(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-e', '--enable', action='store_true')
    arg_parser.add_argument('-d', '--disable', action='store_true')
    arg_parser.add_argument('-p', '--protected', action='append')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args or not (options.enable or options.disable or options.protected):
        self.help_softwarechannel_setorgaccesstree()
        return

    # allow globbing of software channel names
    channels = filter_results(self.list_base_channels(), args)

    if not channels:
        logging.error("Channel does not exist or is not a base channel!")
        self.help_softwarechannel_setorgaccesstree()
        return

    for channel in channels:
        do_softwarechannel_setorgaccess(self, [channel], options)
        for child in self.list_child_channels(parent=channel):
            do_softwarechannel_setorgaccess(self, [child], options)

####################


def help_softwarechannel_regenerateneededcache(self):
    print('softwarechannel_regenerateneededcache: ')
    print('Regenerate the needed errata and package cache for all systems')
    print('')
    print('usage: softwarechannel_regenerateneededcache')


def do_softwarechannel_regenerateneededcache(self, args):
    if self.user_confirm('Are you sure [y/N]: '):
        self.client.channel.software.regenerateNeededCache(self.session)

####################


def help_softwarechannel_regenerateyumcache(self):
    print('softwarechannel_regenerateyumcache: ')
    print('Regenerate the YUM cache for a software channel')
    print('')
    print('''usage: softwarechannel_regenerateyumcache [options] <CHANNEL ...>

    options:
      -f force cache regeneration
    ''')
    print('')


def complete_softwarechannel_regenerateyumcache(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_regenerateyumcache(self, args):
    arg_parser = get_argument_parser()

    arg_parser.add_argument('-f', '--force', action="store_true")

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_regenerateyumcache()
        return

    # allow globbing of software channel names
    channels = filter_results(self.do_softwarechannel_list('', True), args)

    for channel in channels:
        logging.debug('Regenerating YUM cache for %s' % channel)
        self.client.channel.software.regenerateYumCache(self.session, channel, _options.force)

####################
# softwarechannel helper


def is_softwarechannel(self, name):
    if not name:
        return
    return name in self.do_softwarechannel_list(name, True)


def check_softwarechannel(self, name):
    if not name:
        logging.error("no softwarechannel label given")
        return False
    if not self.is_softwarechannel(name):
        logging.error("invalid softwarechannel label " + name)
        return False
    return True


def dump_softwarechannel(self, name, replacedict=None, excludes=None):
    excludes = excludes or []
    content = self.do_softwarechannel_listallpackages(name, doreturn=True)

    content = get_normalized_text(
        content, replacedict=replacedict, excludes=excludes)

    return content

####################


def help_softwarechannel_diff(self):
    print('softwarechannel_diff: diff softwarechannel files')
    print('')
    print('usage: softwarechannel_diff SOURCE_CHANNEL TARGET_CHANNEL')


def complete_softwarechannel_diff(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    return []


def do_softwarechannel_diff(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_softwarechannel_diff()
        return

    source_channel = args[0]
    if not self.check_softwarechannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_softwarechannel_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_softwarechannel_getcorresponding(
            source_channel)
    if not self.check_softwarechannel(target_channel):
        return

    # softwarechannel do not contain references to other components,
    # therefore there is no need to use replace dicts
    source_data = self.dump_softwarechannel(source_channel, None)
    target_data = self.dump_softwarechannel(target_channel, None)

    return diff(source_data, target_data, source_channel, target_channel)

####################


def dump_softwarechannel_errata(self, name):
    errata = self.client.channel.software.listErrata(self.session, name)
    result = []
    for erratum in errata:
        result.append('%s %s' % (
            erratum.get('advisory_name').ljust(14),
            wrap(erratum.get('advisory_synopsis'), 50)[0]))
    result.sort()
    return result


def help_softwarechannel_errata_diff(self):
    print('softwarechannel_errata_diff: diff softwarechannel files')
    print('')
    print('usage: softwarechannel_errata_diff SOURCE_CHANNEL TARGET_CHANNEL')


def complete_softwarechannel_errata_diff(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    return []


def do_softwarechannel_errata_diff(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_softwarechannel_errata_diff()
        return

    source_channel = args[0]
    if not self.check_softwarechannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_softwarechannel_getcorresponding"):
        # try to find the corresponding channel automatically
        target_channel = self.do_softwarechannel_getcorresponding(
            source_channel)
    if not self.check_softwarechannel(target_channel):
        return

    # softwarechannel do not contain references to other components,
    # therefore there is no need to use replace dicts
    source_data = self.dump_softwarechannel_errata(source_channel)
    target_data = self.dump_softwarechannel_errata(target_channel)
    return diff(source_data, target_data, source_channel, target_channel)

####################


def help_softwarechannel_sync(self):
    print('softwarechannel_sync: ')
    print('sync the packages of two software channels')
    print('')
    print('''usage: softwarechannel_sync SOURCE_CHANNEL TARGET_CHANNEL [options])
    -q,--quiet : quiet mode (omits the output of common packages in both channels)''')


def complete_softwarechannel_sync(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    return []


def do_softwarechannel_sync(self, args, deleteFromTarget=True):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-q', '--quiet', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_softwarechannel_sync() if deleteFromTarget else self.help_softwarechannel_mergepackages()
        return

    source_channel = args[0]
    if not self.check_softwarechannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_softwarechannel_getcorresponding"):
        # can a corresponding channel name be found automatically?
        target_channel = self.do_softwarechannel_getcorresponding(
            source_channel)
    if not self.check_softwarechannel(target_channel):
        return

    command = "syncing" if deleteFromTarget else "merging"
    logging.info("%s packages from softwarechannel %s to %s",
                 command, source_channel, target_channel)

    # use API call instead of spacecmd function
    # to get detailed infos about the packages
    # and not just their names
    source_packages = self.client.channel.software.listAllPackages(
        self.session,
        source_channel)
    target_packages = self.client.channel.software.listAllPackages(
        self.session,
        target_channel)

    # get the package IDs
    source_ids = set()
    for package in source_packages:
        try:
            source_ids.add(package['id'])
        except KeyError:
            logging.error("failed to read key id")
            continue

    target_ids = set()
    for package in target_packages:
        try:
            target_ids.add(package['id'])
        except KeyError:
            logging.error("failed to read key id")
            continue
    if not options.quiet:
        print("packages common in both channels:")
        for i in (source_ids & target_ids):
            print(self.get_package_name(i))
        print('')
    else:
        logging.info("Omitting common packages in both specified channels")

    # check for packages only in the source channel
    source_only = source_ids.difference(target_ids)
    if source_only:
        print('packages to add to channel "' + target_channel + '":')
        for i in source_only:
            print(self.get_package_name(i))
        print('')

    # check for packages only in the target channel
    target_only = target_ids.difference(source_ids)
    if target_only and deleteFromTarget:
        print('packages to remove from channel "' + target_channel + '":')
        for i in target_only:
            print(self.get_package_name(i))
        print('')

    if source_only or target_only:
        print("summary:")
        print("  " + source_channel + ": " + str(len(source_ids)).rjust(5) + " packages")
        print("  " + target_channel + ": " + str(len(target_ids)).rjust(5) + " packages")
        print("    add    " + str(
            len(source_only)).rjust(5) + " packages to   " + target_channel)

        if deleteFromTarget:
            print("    remove " + str(
                len(target_only)).rjust(5) + " packages from " + target_channel)

        if not self.user_confirm('Perform these changes to channel ' + target_channel + ' [y/N]:'):
            return

        if deleteFromTarget:
            self.client.channel.software.addPackages(self.session,
                                                    target_channel,
                                                    list(source_only))
            self.client.channel.software.removePackages(self.session,
                                                    target_channel,
                                                    list(target_only))
        else:
            self.client.channel.software.mergePackages(self.session,
                                                    source_channel,
                                                    target_channel)
            
####################


def help_softwarechannel_errata_sync(self):
    print('softwarechannel_errata_sync: ')
    print('sync errata of two software channels')
    print('')
    print('usage: softwarechannel_errata_sync SOURCE_CHANNEL TARGET_CHANNEL')


def complete_softwarechannel_errata_sync(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    return []


def do_softwarechannel_errata_sync(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1 and len(args) != 2:
        self.help_softwarechannel_errata_sync()
        return

    source_channel = args[0]
    if not self.check_softwarechannel(source_channel):
        return

    target_channel = None
    if len(args) == 2:
        target_channel = args[1]
    elif hasattr(self, "do_softwarechannel_getcorresponding"):
        # try to find a corresponding channel name automatically
        target_channel = self.do_softwarechannel_getcorresponding(
            source_channel)
    if not self.check_softwarechannel(target_channel):
        return

    logging.info("syncing errata from softwarechannel " + source_channel +
                 " to " + target_channel)

    source_errata = self.client.channel.software.listErrata(
        self.session, source_channel)
    target_errata = self.client.channel.software.listErrata(
        self.session, target_channel)

    # store unique errata data in a set
    source_ids = set()
    for erratum in source_errata:
        try:
            source_ids.add(erratum.get('advisory_name'))
        except KeyError:
            logging.error("failed to read key id")
            continue

    target_ids = set()
    for erratum in target_errata:
        try:
            target_ids.add(erratum.get('advisory_name'))
        except KeyError:
            logging.error("failed to read key id")
            continue

    print("errata common in both channels:")
    for i in (source_ids & target_ids):
        print(i)
    print('')

    # check for errata only in the source channel
    source_only = list(source_ids.difference(target_ids))
    source_only.sort()
    if source_only:
        print('errata to add to channel "' + target_channel + '":')
        for i in source_only:
            print(i)
        print('')

    # check for errata only in the target channel
    target_only = list(target_ids.difference(source_ids))
    target_only.sort()
    if target_only:
        print('errata to remove from channel "' + target_channel + '":')
        for i in target_only:
            print(i)
        print('')

    if source_only or target_only:
        print("summary:")
        print("  " + source_channel + ": " + str(len(source_ids)).rjust(5), "errata")
        print("  " + target_channel + ": " + str(len(target_ids)).rjust(5), "errata")
        print("    add   ", str(
            len(source_only)).rjust(5), "errata to  ", target_channel)
        print("    remove", str(
            len(target_only)).rjust(5), "errata from", target_channel)

        if not self.user_confirm('Perform these changes to channel ' + target_channel + ' [y/N]:'):
            return

        for erratum in source_only:
            print(erratum)
            self.client.errata.publish(self.session, erratum, [target_channel])
        # alternative:
        # channel.software.mergeErrata: Merges all errata from one channel into another

        # channel.software.removeErrata:
        #    string channelLabel - target channel.
        #    array:
        #      string - advisoryName - name of an erratum to remove
        #    boolean removePackages - True to remove packages from the channel
        self.client.channel.software.removeErrata(self.session, target_channel,
                                                  target_only, False)

####################


def help_softwarechannel_errata_merge(self):
    print('softwarechannel_errata_merge: ')
    print('merge errata of one software channel into another')
    print('')
    print('usage:   softwarechannel_errata_merge SOURCE_CHANNEL TARGET_CHANNEL [FROM_DATE] [TO_DATE]')
    print('example: softwarechannel_errata_merge OldSoftChannel NewSoftChannel 2018-01-01')
    print('note:    Providing only FROM_DATE will merge all errata since that date.')
    print('note:    When no date is provided, all errata will be merged.')

def complete_softwarechannel_errata_merge(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')
    args = len(parts)

    if args == 2:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    if args == 3:
        return tab_completer(self.do_softwarechannel_list('', True), text)
    return []

def do_softwarechannel_errata_merge(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    FROM_DATE = datetime.strptime("1970-01-01", "%Y-%m-%d")
    TO_DATE   = datetime.now() + timedelta(1)   #tomorrow

    if len(args) < 2 or len(args) > 4:
        self.help_softwarechannel_errata_merge()
        return

    if len(args) >= 3:
        try:
            FROM_DATE = datetime.strptime(args[2], "%Y-%m-%d")
        except ValueError:
            print('wrong dateformat: ' + args[2] + ' please specify as: YYYY-MM-DD')
            return

    if len(args) == 4:
        try:
            TO_DATE = datetime.strptime(args[3], "%Y-%m-%d")
        except ValueError:
            print('wrong dateformat: ' + args[3] + ' please specify as: YYYY-MM-DD')
            return

    source_channel = args[0]
    target_channel = args[1]

    if not self.check_softwarechannel(source_channel) or not self.check_softwarechannel(target_channel):
        return

    logging.info("merging errata from softwarechannel " + source_channel +
                 " to " + target_channel)

    source_errata = self.client.channel.software.listErrata(
        self.session, source_channel)

    # filter out errata which are not in the specified timeframe (if given)
    for erratum in source_errata[:]:
        erratum_date = datetime.strptime(erratum['issue_date'].split(' ')[0], "%Y-%m-%d")

        if not (FROM_DATE <= erratum_date <= TO_DATE):
            source_errata.remove(erratum)

    target_errata = self.client.channel.software.listErrata(
        self.session, target_channel)

    # store unique errata data in a set
    source_ids = set()
    for erratum in source_errata:
        try:
            source_ids.add(erratum.get('advisory_name'))
        except KeyError:
            logging.error("failed to read key id")
            continue

    target_ids = set()
    for erratum in target_errata:
        try:
            target_ids.add(erratum.get('advisory_name'))
        except KeyError:
            logging.error("failed to read key id")
            continue

    # check for errata only in the source channel
    source_only = list(source_ids.difference(target_ids))

    if len(source_only) == 0:
        print('{} contains no errata which is not already present in {}'.format(source_channel, target_channel))
        return

    source_only.sort()
    if source_only:
        print('errata to add to channel {}: '.format(target_channel))
        for i in source_only:
            print(i)
        print('')

    print("summary:")
    print("  " + source_channel + ": " + str(len(source_ids)).rjust(5) + " errata")
    print("  " + target_channel + ": " + str(len(target_ids)).rjust(5) + " errata")
    print ("    add   " + str(
        len(source_only)).rjust(5) + " errata to  " + target_channel)

    if not self.user_confirm('Perform these changes to channel {} [y/N]:'.format(target_channel)):
        return
    
    self.client.channel.software.mergeErrata(self.session, source_channel, target_channel, source_only)

####################


def help_softwarechannel_syncrepos(self):
    print('softwarechannel_syncrepos: ')
    print('Sync users repos for a software channel')
    print('')
    print('usage: softwarechannel_syncrepos <CHANNEL ...>')
    print('Options:')
    print('    -e/--no-errata : Do not sync errata')
    print('    -f/--fail : Terminate upon any error')
    print('    -k/--sync-kickstart : Create kickstartable tree')
    print('    -l/--latest : Only download latest package versions when repo syncs')


def complete_softwarechannel_syncrepos(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_syncrepos(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-e', '--no-errata', action='store_true', default=False)
    arg_parser.add_argument('-f', '--fail', action='store_true', default=False)
    arg_parser.add_argument('-k', '--sync-kickstart', action='store_true', default=False)
    arg_parser.add_argument('-l', '--latest', action='store_true', default=False)

    (args, options) = parse_command_arguments(args, arg_parser)

    params = dict((i.replace('_', '-'), getattr(options, i)) for i in ['no_errata', 'fail', 'sync_kickstart', 'latest'])

    if not args:
        self.help_softwarechannel_syncrepos()
        return

    # allow globbing of software channel names
    channels = filter_results(self.do_softwarechannel_list('', True), args)

    for channel in channels:
        logging.debug('Syncing repos for %s' % channel)
        self.client.channel.software.syncRepo(self.session, channel, params)

####################


def help_softwarechannel_setsyncschedule(self):
    print('softwarechannel_setsyncschedule: ')
    print('Sets the repo sync schedule for a software channel')
    print('')
    print('usage: softwarechannel_setsyncschedule <CHANNEL> <SCHEDULE>')
    print('Options:')
    print('    -e/--no-errata : Do not sync errata')
    print('    -f/--fail : Terminate upon any error')
    print('    -k/--sync-kickstart : Create kickstartable tree')
    print('    -l/--latest : Only download latest package versions when repo syncs')
    print('')
    print('The schedule is specified in Quartz CronTrigger format without enclosing quotes.')
    print('For example, to set a schedule of every day at 1am, <SCHEDULE> would be 0 0 1 * * ?')
    print('If <SCHEDULE> is left empty, it will be disabled.')
    print('')


def complete_softwarechannel_setsyncschedule(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_setsyncschedule(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-e', '--no-errata', action='store_true', default=False)
    arg_parser.add_argument('-f', '--fail', action='store_true', default=False)
    arg_parser.add_argument('-k', '--sync-kickstart', action='store_true', default=False)
    arg_parser.add_argument('-l', '--latest', action='store_true', default=False)

    # Set glob = false, otherwise this will generate a com.redhat.rhn.taskomatic.InvalidParamException: Cron trigger.
    (args, options) = parse_command_arguments(args, arg_parser, glob=False)

    params = dict((i.replace('_', '-'), getattr(options, i)) for i in ['no_errata', 'fail', 'sync_kickstart', 'latest'])

    if not len(args) in [1, 7]:
        self.help_softwarechannel_setsyncschedule()
        return

    channel = args[0]
    schedule = ' '.join(args[1:]) if len(args) == 7 else ''

    self.client.channel.software.syncRepo(self.session, channel, schedule, params)

####################


def help_softwarechannel_removesyncschedule(self):
    print('softwarechannel_removesyncschedule: ')
    print('Removes the repo sync schedule for a software channel')
    print('')
    print('usage: softwarechannel_removesyncschedule <CHANNEL>')


def complete_softwarechannel_removesyncschedule(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_removesyncschedule(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args) == 1:
        self.help_softwarechannel_removesyncschedule()
        return

    channel = args[0]

    self.client.channel.software.syncRepo(self.session, channel, '')

####################

def help_softwarechannel_listsyncschedule(self):
    print('softwarechannel_listsyncschedule: List sync schedules for all software channels')
    print('usage:')
    print('softwarechannel_listsyncschedule : List all channels')


def do_softwarechannel_listsyncschedule(self, args):

    # Get a list of all channels and sync schedules
    channels = self.client.channel.listAllChannels(self.session)
    schedules = self.client.taskomatic.org.listActiveSchedules(self.session)

    chan_name = {}
    chan_sched = {}

    # Build an array of channel names indexed by internal channel id number
    for c in channels:
        chan_name[ c['id'] ] = c['label']
        chan_sched[ c['id'] ] = ''

    # Build an array of schedules indexed by internal channel id number
    for s in schedules:
        chan_sched[int(s['data_map']['channel_id'])] = s['cron_expr']

    # Print headers
    csched_fmt = '{0:>5s}  {1:<40s} {2:<20s}'
    print(csched_fmt.format('key', 'Channel Name', 'Update Schedule'))
    print(csched_fmt.format('-----', '---------------------', '---------------'))

    # Sort and print(the channel names and associated repo-sync schedule (if any))
    for key,value in sorted(chan_name.items()):
        print(csched_fmt.format(str(key), value, chan_sched[int(key)]))

####################

def help_softwarechannel_addrepo(self):
    print('softwarechannel_addrepo: Add a repo to a software channel')
    print('usage: softwarechannel_addrepo CHANNEL REPO')


def complete_softwarechannel_addrepo(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    elif len(parts) == 3:
        return tab_completer(self.do_repo_list('', True), text)


def do_softwarechannel_addrepo(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_softwarechannel_addrepo()
        return

    channel = args[0]
    repo = args[1]

    self.client.channel.software.associateRepo(self.session, channel, repo)

####################


def help_softwarechannel_removerepo(self):
    print('softwarechannel_removerepo: Remove a repo from a software channel')
    print('usage: softwarechannel_removerepo CHANNEL REPO')


def complete_softwarechannel_removerepo(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_softwarechannel_list('', True),
                             text)
    elif len(parts) == 3:
        try:
            details = self.client.channel.software.getDetails(self.session,
                                                              parts[1])
            repos = [r.get('label') for r in details.get('contentSources')]
        except xmlrpclib.Fault:
            return

        return tab_completer(repos, text)


def do_softwarechannel_removerepo(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_softwarechannel_removerepo()
        return

    channel = args[0]
    repo = args[1]

    self.client.channel.software.disassociateRepo(self.session, channel, repo)

####################


def help_softwarechannel_listrepos(self):
    print('softwarechannel_listrepos: List the repos for a software channel')
    print('usage: softwarechannel_listrepos CHANNEL')


def complete_softwarechannel_listrepos(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_listrepos(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_listrepos()
    else:
        details = self.client.channel.software.getDetails(self.session, args[0])
        repos = [r.get('label') for r in details.get('contentSources')]

        if repos:
            print('\n'.join(sorted(repos)))
        else:
            self.help_softwarechannel_listrepos()

####################


def help_softwarechannel_mirrorpackages(self):
    print('softwarechannel_mirrorpackages: Download packages of a given channel')
    print('usage: softwarechannel_mirrorpackages CHANNEL')
    print('Options:')
    print('    -l/--latest : Only mirror latest package version')


def complete_softwarechannel_mirrorpackages(self, text, line, beg, end):
    return tab_completer(self.do_softwarechannel_list('', True), text)


def do_softwarechannel_mirrorpackages(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-l', '--latest', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_softwarechannel_mirrorpackages()
        return
    channel = args[0]
    if not (options.latest):
        packages = \
            self.client.channel.software.listAllPackages(self.session, channel)
    else:
        packages = \
            self.client.channel.software.listLatestPackages(
                self.session, channel)

    for package in packages:
        package_url = self.client.packages.getPackageUrl(
            self.session, package['id'])
        package_file = self.client.packages.getDetails(
            self.session, package['id']).get('file')
        if os.path.isfile(package_file):
            print("Skipping", package_file)
        else:
            print("Fetching package", package_file)
            try:
                urlretrieve(package_url, package_file)
            except ContentTooShortError:
                logging.error(
                    "Received package %s from channel %s is broken. Content is too short",
                    package_file, channel)
            except IOError:
                logging.error("Could not fetch package %s from channel %s" %
                              (package_file, channel))
07070100000029000081B40000000000000000000000015DA8415F000016D6000000000000000000000000000000000000001D00000000spacecmd/src/spacecmd/ssm.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

from spacecmd.utils import *


def help_ssm(self):
    print('The System Set Manager (SSM) is a group of systems that you ')
    print('can perform tasks on as a whole.')
    print('')
    print('Adding Systems:')
    print('> ssm_add group:rhel5-x86_64')
    print('> ssm_add channel:rhel-x86_64-server-5')
    print('> ssm_add search:device:vmware')
    print('> ssm_add host.example.com')
    print('')
    print('Intersections:')
    print('> ssm_add group:rhel5-x86_64')
    print('> ssm_intersect group:web-servers')
    print('')
    print('Using the SSM:')
    print('> system_installpackage ssm zsh')
    print('> system_runscript ssm')

####################


def help_ssm_add(self):
    print('ssm_add: Add systems to the SSM')
    print('usage: ssm_add <SYSTEMS>')
    print('')
    print("see 'help ssm' for more details")
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_ssm_add(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_ssm_add(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_ssm_add()
        return

    systems = self.expand_systems(args)

    if not systems:
        logging.warning('No systems found')
        return

    for system in systems:
        if system in self.ssm:
            logging.warning('%s is already in the list' % system)
            continue
        else:
            self.ssm[system] = self.get_system_id(system)
            logging.debug('Added %s' % system)

    if self.ssm:
        logging.debug('Systems Selected: %i' % len(self.ssm))

    # save the SSM for use between sessions
    save_cache(self.ssm_cache_file, self.ssm)

####################


def help_ssm_intersect(self):
    print('ssm_intersect: Replace the current SSM with the intersection')
    print('               of the current list of systems and the list of')
    print('               systems passed as arguments')
    print('usage: ssm_intersect <SYSTEMS>')
    print('')
    print("see 'help ssm' for more details")
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_ssm_intersect(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_ssm_intersect(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_ssm_intersect()
        return

    systems = self.expand_systems(args)

    if not systems:
        logging.warning('No systems found')
        return

    # tmp_ssm placeholder to gather systems that are both in original ssm
    # selection and newly selected group
    tmp_ssm = {}
    for system in systems:
        if system in self.ssm:
            logging.debug('%s is in both groups: leaving in SSM' % system)
            tmp_ssm[system] = self.ssm[system]

    # set self.ssm to tmp_ssm, which now holds the intersection
    self.ssm = tmp_ssm

    # save the SSM for use between sessions
    save_cache(self.ssm_cache_file, self.ssm)

    if self.ssm:
        logging.debug('Systems Selected: %i' % len(self.ssm))

####################


def help_ssm_remove(self):
    print('ssm_remove: Remove systems from the SSM')
    print('usage: ssm_remove <SYSTEMS>')
    print('')
    print("see 'help ssm' for more details")
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_ssm_remove(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_ssm_remove(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_ssm_remove()
        return

    systems = self.expand_systems(args)

    if not systems:
        logging.warning('No systems found')
        return

    for system in systems:
        # double-check for existance in case of duplicate names
        if system in self.ssm:
            logging.debug('Removed %s' % system)
            del self.ssm[system]

    logging.debug('Systems Selected: %i' % len(self.ssm))

    # save the SSM for use between sessions
    save_cache(self.ssm_cache_file, self.ssm)

####################


def help_ssm_list(self):
    print('ssm_list: List the systems currently in the SSM')
    print('usage: ssm_list')
    print('')
    print("see 'help ssm' for more details")


def do_ssm_list(self, args):
    systems = sorted(self.ssm)

    if systems:
        print('\n'.join(systems))
        logging.debug('Systems Selected: %i' % len(systems))

####################


def help_ssm_clear(self):
    print('ssm_clear: Remove all systems from the SSM')
    print('usage: ssm_clear')


def do_ssm_clear(self, args):
    self.ssm = {}

    # save the SSM for use between sessions
    save_cache(self.ssm_cache_file, self.ssm)
0707010000002A000081B40000000000000000000000015DA8415F0001D9D1000000000000000000000000000000000000002000000000spacecmd/src/spacecmd/system.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2013--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# unused argument
# pylint: disable=W0613

# wildcard import
# pylint: disable=W0401,W0614

# invalid function name
# pylint: disable=C0103

import shlex
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from operator import itemgetter
from xml.parsers.expat import ExpatError
from spacecmd.utils import *

__PKG_COMPARISONS = {0: 'Same',
                     1: 'Only here',
                     2: 'Newer here',
                     3: 'Only there',
                     4: 'Newer there'}


def print_package_comparison(self, results):
    max_name = max_length(map(itemgetter('package_name'), results), minimum=7)

    # sometimes 'this_system' or 'other_system' can be None
    tmp_this = []
    tmp_other = []
    for item in results:
        tmp_this.append(str(item.get('this_system')))
        tmp_other.append(str(item.get('other_system')))

    max_this = max_length(tmp_this, minimum=11)
    max_other = max_length(tmp_other, minimum=12)

    max_comparison = 10

    # print(headers)
    print('%s  %s  %s  %s' % (
        'Package'.ljust(max_name),
        'This System'.ljust(max_this),
        'Other System'.ljust(max_other),
        'Difference'.ljust(max_comparison)))

    print('%s  %s  %s  %s' % (
        '-' * max_name,
        '-' * max_this,
        '-' * max_other,
        '-' * max_comparison))

    for item in results:
        # don't show packages that are the same
        if item.get('comparison') == 0:
            continue

        print('%s  %s  %s  %s' % (
            item.get('package_name').ljust(max_name),
            str(item.get('this_system')).ljust(max_this),
            str(item.get('other_system')).ljust(max_other),
            __PKG_COMPARISONS[item.get('comparison')]))

####################


def manipulate_child_channels(self, args, remove=False):
    arg_parser = get_argument_parser()
    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        if remove:
            self.help_system_removechildchannels()
        else:
            self.help_system_addchildchannels()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    new_channels = args

    print('Systems')
    print('-------')
    print('\n'.join(sorted(["%s" % x for x in systems])))
    print('')

    if remove:
        print('Removing Channels')
        print('-----------------')
    else:
        print('Adding Channels')
        print('---------------')

    print('\n'.join(sorted(new_channels)))

    if not self.user_confirm():
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        child_channels = \
            self.client.system.listSubscribedChildChannels(self.session,
                                                           system_id)

        child_channels = [c.get('label') for c in child_channels]

        if remove:
            for channel in new_channels:
                if channel in child_channels:
                    child_channels.remove(channel)
        else:
            for channel in new_channels:
                if channel not in child_channels:
                    child_channels.append(channel)

        self.client.system.setChildChannels(self.session,
                                            system_id,
                                            child_channels)

####################


def help_system_list(self):
    print('system_list: List all system profiles')
    print('usage: system_list')


def do_system_list(self, args, doreturn=False):
    if doreturn:
        return self.get_system_names()
    else:
        if self.get_system_names():
            print('\n'.join(sorted(['%s : %s' % (v, k) for k, v in self.get_system_names_ids().items()])))

####################


def help_system_reboot(self):
    print('system_reboot: Reboot a system')
    print('''usage: system_reboot <SYSTEMS> [options])

options:
  -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_reboot(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_reboot(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_reboot()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    print('')

    print('Start Time: %s' % options.start_time)
    print('')
    print('Systems')
    print('-------')
    print('\n'.join(sorted(systems)))

    if not self.user_confirm('Reboot these systems [y/N]:'):
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.scheduleReboot(self.session, system_id, options.start_time)

####################


def help_system_search(self):
    print('system_search: List systems that match the given criteria')
    print('usage: system_search QUERY')
    print('')
    print('Available Fields:')
    print('\n'.join(self.SYSTEM_SEARCH_FIELDS))
    print('')
    print('Examples:')
    print('> system_search device:vmware')
    print('> system_search ip:192.168.82')


def do_system_search(self, args, doreturn=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_system_search()
        return

    query = args[0]

    if re.search(':', query):
        try:
            (field, value) = query.split(':')
        except ValueError:
            logging.error('Invalid query')
            return []
    else:
        field = 'name'
        value = query

    if not value:
        logging.warning('Invalid query')
        return []

    results = []
    if field == 'name':
        results = self.client.system.search.nameAndDescription(self.session,
                                                               value)
        key = 'name'
    elif field == 'id':
        # build an array of key/value pairs from our local system cache
        self.generate_system_cache()
        results = [{'id': k, 'name': self.all_systems[k]}
                   for k in self.all_systems]
        key = 'id'
    elif field == 'ip':
        results = self.client.system.search.ip(self.session, value)
        key = 'ip'
    elif field == 'hostname':
        results = self.client.system.search.hostname(self.session, value)
        key = 'hostname'
    elif field == 'device':
        results = self.client.system.search.deviceDescription(self.session,
                                                              value)
        key = 'hw_description'
    elif field == 'vendor':
        results = self.client.system.search.deviceVendorId(self.session,
                                                           value)
        key = 'hw_vendor_id'
    elif field == 'driver':
        results = self.client.system.search.deviceDriver(self.session,
                                                         value)
        key = 'hw_driver'
    elif field == 'uuid':
        results = self.client.system.search.uuid(self.session, value)
        key = 'uuid'
    else:
        logging.warning('Invalid search field')
        return []

    systems = []
    max_size = 0
    for s in results:
        # only use real matches, not the fuzzy ones we get back
        if re.search(value, "%s" % s.get(key), re.I):
            if len(s.get('name')) > max_size:
                max_size = len(s.get('name'))

            systems.append((s.get('name'), s.get(key), s.get('id')))

    if doreturn:
        return [s[2] for s in systems]
    else:
        if systems:
            for s in sorted(systems):
                if key == 'name':
                    print(s[0])
                else:
                    print('%s  %s' % (s[0].ljust(max_size),
                                      str(s[1]).strip()))

####################


def help_system_runscript(self):
    print('system_runscript: Schedule a script to run on the list of')
    print('                  systems provided')
    print('''usage: system_runscript <SYSTEMS> [options])

options:
  -u USER
  -g GROUP
  -t TIMEOUT
  -s START_TIME
  -l LABEL
  -f FILE''')
    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_runscript(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_runscript(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-u', '--user')
    arg_parser.add_argument('-g', '--group')
    arg_parser.add_argument('-t', '--timeout')
    arg_parser.add_argument('-s', '--start-time')
    arg_parser.add_argument('-l', '--label')
    arg_parser.add_argument('-f', '--file')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_runscript()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    if not systems:
        logging.warning('No systems selected')
        return

    if is_interactive(options):
        options.user = prompt_user('User [root]:')
        options.group = prompt_user('Group [root]:')

        # defaults
        if not options.user:
            options.user = 'root'
        if not options.group:
            options.group = 'root'

        try:
            options.timeout = prompt_user('Timeout (in seconds) [600]:')
            if options.timeout:
                options.timeout = int(options.timeout)
            else:
                options.timeout = 600
        except ValueError:
            logging.error('Invalid timeout')
            return

        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)

        options.label = prompt_user('Label/Short Description [default]:')
        if options.label == "":
            options.label = None

        options.file = prompt_user('Script File [create]:')

        # read the script provided by the user
        if options.file:
            keep_script_file = True

            script_contents = read_file(os.path.abspath(options.file))
        else:
            # have the user write their script
            (script_contents, options.file) = editor('#!/bin/bash')
            keep_script_file = False

        if not script_contents:
            logging.error('No script provided')
            return
    else:
        if not options.user:
            options.user = 'root'
        if not options.group:
            options.group = 'root'
        if not options.label:
            options.label = None
        if not options.timeout:
            options.timeout = 600
        else:
            options.timeout = int(options.timeout)
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

        if not options.file:
            logging.error('A script file is required')
            return

        script_contents = read_file(options.file)
        keep_script_file = True

    # display a summary
    print('')
    print('User:       %s' % options.user)
    print('Group:      %s' % options.group)
    print('Timeout:    %i seconds' % options.timeout)
    print('Start Time: %s' % options.start_time)
    print('')
    if options.label:
        print('Label:      %s' % options.label)
    print('Script Contents')
    print('---------------')
    print(script_contents)

    print('Systems')
    print('-------')
    print('\n'.join(sorted(systems)))

    # have the user confirm
    if not self.user_confirm():
        return

    scheduled = 0

    if self.check_api_version('10.11'):
        logging.debug('Scheduling all systems for the same action')

        # schedule all systems for the same action
        system_ids = [self.get_system_id(s) for s in systems]
        if not options.label:
            action_id = self.client.system.scheduleScriptRun(self.session,
                                                             system_ids,
                                                             options.user,
                                                             options.group,
                                                             options.timeout,
                                                             script_contents,
                                                             options.start_time)
        else:
            action_id = self.client.system.scheduleScriptRun(self.session,
                                                             options.label,
                                                             system_ids,
                                                             options.user,
                                                             options.group,
                                                             options.timeout,
                                                             script_contents,
                                                             options.start_time)


        logging.info('Action ID: %i' % action_id)
        scheduled = len(system_ids)
    else:
        # older versions of the API require each system to be
        # scheduled individually
        for system in systems:
            system_id = self.get_system_id(system)
            if not system_id:
                continue

            try:
                action_id = \
                    self.client.system.scheduleScriptRun(self.session,
                                                         system_id,
                                                         options.user,
                                                         options.group,
                                                         options.timeout,
                                                         script_contents,
                                                         options.start_time)

                logging.info('Action ID: %i' % action_id)
                scheduled += 1
            except xmlrpclib.Fault as detail:
                logging.debug(detail)
                logging.error('Failed to schedule %s' % system)

    logging.info('Scheduled: %i system(s)' % scheduled)

    # don't delete a pre-existing script that the user provided
    if not keep_script_file:
        try:
            os.remove(options.file)
        except OSError:
            logging.error('Could not remove %s' % options.file)

####################


def help_system_listhardware(self):
    print('system_listhardware: List the hardware details of a system')
    print('usage: system_listhardware <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listhardware(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listhardware(self, args):
    arg_parser = get_argument_parser()
    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listhardware()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        cpu = self.client.system.getCpu(self.session, system_id)
        memory = self.client.system.getMemory(self.session, system_id)
        devices = self.client.system.getDevices(self.session, system_id)
        network = self.client.system.getNetworkDevices(self.session,
                                                       system_id)

        # Solaris systems don't have these value s
        for v in ('cache', 'vendor', 'family', 'stepping'):
            if not cpu.get(v):
                cpu[v] = ''

        try:
            dmi = self.client.system.getDmi(self.session, system_id)
        except ExpatError:
            dmi = None

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        if network:
            print('Network')
            print('-------')

            count = 0
            for device in network:
                if count:
                    print('')
                count += 1

                print('Interface:   %s' % device.get('interface'))
                print('MAC Address: %s' % device.get('hardware_address').upper())
                print('IP Address:  %s' % device.get('ip'))
                print('Netmask:     %s' % device.get('netmask'))
                print('Broadcast:   %s' % device.get('broadcast'))
                print('Module:      %s' % device.get('module'))

            print('')

        print('CPU')
        print('---')
        print('Count:    %i' % cpu.get('count'))
        print('Arch:     %s' % cpu.get('arch'))
        print('MHz:      %s' % cpu.get('mhz'))
        print('Cache:    %s' % cpu.get('cache'))
        print('Vendor:   %s' % cpu.get('vendor'))
        print('Model:    %s' % re.sub(r'\s+', ' ', cpu.get('model')))

        print('')
        print('Memory')
        print('------')
        print('RAM:  %i' % memory.get('ram'))
        print('Swap: %i' % memory.get('swap'))

        if dmi:
            print('')
            print('DMI')
            print('Vendor:       %s' % dmi.get('vendor'))
            print('System:       %s' % dmi.get('system'))
            print('Product:      %s' % dmi.get('product'))
            print('Board:        %s' % dmi.get('board'))

            print('')
            print('Asset')
            print('-----')
            for asset in dmi.get('asset').split(') ('):
                print(re.sub(r'\)|\(', '', asset))

            print('')
            print('BIOS Release: %s' % dmi.get('bios_release'))
            print('BIOS Vendor:  %s' % dmi.get('bios_vendor'))
            print('BIOS Version: %s' % dmi.get('bios_version'))

        if devices:
            print('')
            print('Devices')
            print('-------')

            count = 0
            for device in devices:
                if count:
                    print('')
                count += 1

                if device.get('description') is None:
                    print('Description: None')
                else:
                    print('Description: %s' % (
                        wrap(device.get('description'), 60)[0]))
                print('Driver:      %s' % device.get('driver'))
                print('Class:       %s' % device.get('device_class'))
                print('Bus:         %s' % device.get('bus'))

####################


def help_system_installpackage(self):
    print('system_installpackage: Install a package on a system')
    print('''usage: system_installpackage <SYSTEMS> <PACKAGE ...> [options])

options:
    -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_installpackage(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(), text)


def do_system_installpackage(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_installpackage()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()

        # remove 'ssm' from the argument list
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    packages_to_install = args

    # get the ID for each system
    system_ids = []
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue
        system_ids.append(system_id)

    jobs = {}

    if self.check_api_version('10.11'):
        for package in packages_to_install:
            logging.debug('Finding the latest version of %s' % package)

            avail_packages = \
                self.client.system.listLatestAvailablePackage(self.session,
                                                              system_ids,
                                                              package)

            for system in avail_packages:
                system_id = system.get('id')
                if system_id not in jobs:
                    jobs[system_id] = []

                # add this package to the system's queue
                jobs[system_id].append(system.get('package').get('id'))
    else:
        # XXX: Satellite 5.3 compatibility
        for system_id in system_ids:
            logging.debug('Getting available packages for %s' %
                          self.get_system_name(system_id))

            avail_packages = \
                self.client.system.listLatestInstallablePackages(self.session,
                                                                 system_id)

            for package in avail_packages:
                if package.get('name') in packages_to_install:
                    if system_id not in jobs:
                        jobs[system_id] = []

                    jobs[system_id].append(package.get('id'))

    if not jobs:
        logging.warning('No packages to install')
        return

    add_separator = False

    warnings = []
    for system_id in jobs:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        # warn the user if the request can not be 100% fulfilled
        if len(jobs[system_id]) != len(packages_to_install):
            # stash the warnings and show at the end so the user can see them
            warnings.append(system_id)

        print('%s:' % self.get_system_name(system_id))
        for package_id in jobs[system_id]:
            print(self.get_package_name(package_id))

    # show the warnings to the user
    if warnings:
        print('')
    for system_id in warnings:
        logging.warning('%s does not have access to all requested packages' %
                        self.get_system_name(system_id))

    print('')
    print('Start Time: %s' % options.start_time)

    if not self.user_confirm('Install these packages [y/N]:'):
        return

    scheduled = 0
    for system_id in jobs:
        try:
            self.client.system.schedulePackageInstall(self.session,
                                                      system_id,
                                                      jobs[system_id],
                                                      options.start_time)

            scheduled += 1
        except xmlrpclib.Fault:
            logging.error('Failed to schedule %s' % self.get_system_name(system_id))

    logging.info('Scheduled %i system(s)' % scheduled)

####################


def help_system_removepackage(self):
    print('system_removepackage: Remove a package from a system')
    print('''usage: system_removepackage <SYSTEMS> <PACKAGE ...> [options])

options:
    -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_removepackage(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(), text)


def do_system_removepackage(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_removepackage()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()

        # remove 'ssm' from the argument list
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    package_list = args

    # get all matching package names
    logging.debug('Finding matching packages')
    matching_packages = \
        filter_results(self.get_package_names(True), package_list, True)

    jobs = {}
    for package_name in matching_packages:
        logging.debug('Finding systems with %s' % package_name)

        installed_systems = {}
        for package_id in self.get_package_id(package_name):
            for system in self.client.system.listSystemsWithPackage(self.session, package_id):
                installed_systems[system.get('name')] = package_id

        # each system has a list of packages to remove so that only one
        # API call needs to be made to schedule all the package removals
        # for each system
        for system in systems:
            if system in installed_systems.keys():
                if system not in jobs:
                    jobs[system] = []

                jobs[system].append(installed_systems[system])

    add_separator = False

    for system in jobs:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('%s:' % system)
        for package in jobs[system]:
            print(self.get_package_name(package))

    if not jobs:
        return

    print('')
    print('Start Time: %s' % options.start_time)

    if not self.user_confirm('Remove these packages [y/N]:'):
        return

    scheduled = 0
    for system in jobs:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        try:
            action_id = self.client.system.schedulePackageRemove(self.session,
                                                                 system_id,
                                                                 jobs[system],
                                                                 options.start_time)

            logging.info('Action ID: %i' % action_id)
            scheduled += 1
        except xmlrpclib.Fault:
            logging.error('Failed to schedule %s' % system)

    logging.info('Scheduled %i system(s)' % scheduled)

####################


def help_system_upgradepackage(self):
    print('system_upgradepackage: Upgrade a package on a system')
    print('''usage: system_upgradepackage <SYSTEMS> <PACKAGE ...>|* [options]')

options:
    -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_upgradepackage(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.get_package_names(), text)


def do_system_upgradepackage(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    # this will come handy for individual packages, as we call
    # self.do_system_installpackage anyway
    orig_args = args

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_upgradepackage()
        return

    # install and upgrade for individual packages are the same
    if not '.*' in args[1:]:
        return self.do_system_installpackage(orig_args)

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()

        # remove 'ssm' from the argument list
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    # make a dictionary of each system and the package IDs to install
    jobs = {}
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        packages = \
            self.client.system.listLatestUpgradablePackages(self.session,
                                                            system_id)

        if packages:
            package_ids = [p.get('to_package_id') for p in packages]
            jobs[system] = package_ids
        else:
            logging.warning('No upgrades available for %s' % system)

    if not jobs:
        return

    add_separator = False

    for system in jobs:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print(system)
        print('-' * len(system))

        # build a temporary list so we can sort by package name
        package_names = []
        for package in jobs[system]:
            name = self.get_package_name(package)

            if name:
                package_names.append(name)
            else:
                logging.error("Couldn't get name for package %i" % package)

        print('\n'.join(sorted(package_names)))

    print('')
    print('Start Time: %s' % options.start_time)

    if not self.user_confirm('Upgrade these packages [y/N]:'):
        return

    scheduled = 0
    for system in jobs:
        system_id = self.get_system_id(system)

        try:
            self.client.system.schedulePackageInstall(self.session,
                                                      system_id,
                                                      jobs[system],
                                                      options.start_time)

            scheduled += 1
        except xmlrpclib.Fault:
            logging.error('Failed to schedule %s' % system)

    logging.info('Scheduled %i system(s)' % scheduled)

####################


def help_system_listupgrades(self):
    print('system_listupgrades: List the available upgrades for a system')
    print('usage: system_listupgrades <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listupgrades(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listupgrades(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listupgrades()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        packages = \
            self.client.system.listLatestUpgradablePackages(self.session,
                                                            system_id)

        if not packages:
            logging.warning('No upgrades available for %s' % system)
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print(system)
            print('-' * len(system))

        latest_packages = filter_latest_packages(packages, 'to_version', 'to_release', 'to_epoch')

        for package in sorted(latest_packages.values(), key=itemgetter('name')):
            print(build_package_names({
                'name': package['name'],
                'version': package['to_version'],
                'release': package['to_release'],
                'epoch': package['to_epoch'],
                'arch': package['to_arch']
            }))

####################


def help_system_listinstalledpackages(self):
    print('system_listinstalledpackages: List the installed packages on a')
    print('                              system')
    print('usage: system_listinstalledpackages <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listinstalledpackages(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listinstalledpackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listinstalledpackages()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        packages = self.client.system.listPackages(self.session,
                                                   system_id)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        print('\n'.join(build_package_names(packages)))

####################


def help_system_listconfigchannels(self):
    print('system_listconfigchannels: List the config channels of a system')
    print('usage: system_listconfigchannels <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listconfigchannels(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listconfigchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listconfigchannels()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        try:
            channels = self.client.system.config.listChannels(self.session,
                                                              system_id)
        except xmlrpclib.Fault:
            logging.warning('%s does not support configuration channels' %
                            system)
            continue

        print('\n'.join([c.get('label') for c in channels]))

####################


def print_configfiles(self, quiet, filelist):

    # Figure out correct indentation to allow pretty table output
    max_path = max_length([f['path'] for f in filelist], minimum=10)
    max_type = max_length(["file", "directory", "symlink"], minimum=10)
    max_label = max_length([f['channel_label'] for f in filelist], minimum=15)

    # print(header when not in quiet mode)
    if not quiet:
        print('%s  %s  %s' % (
            'path'.ljust(max_path),
            'type'.ljust(max_type),
            'label/type'.ljust(max_label)))

        print('%s  %s  %s' % (
            '-' * max_path,
            '-' * max_type,
            '-' * max_label))

    for f in filelist:
        print('%s  %s  %s' % (f['path'].ljust(max_path),
                              f['type'].ljust(max_type),
                              f['channel_label'].ljust(max_label)))


def help_system_listconfigfiles(self):
    print('system_listconfigfiles: List the managed config files of a system')
    print('''usage: system_listconfigfiles <SYSTEMS>')
options:
  -s/--sandbox : list only system-sandbox files
  -l/--local   : list only locally managed files
  -c/--central : list only centrally managed files
  -q/--quiet   : quiet mode (omits the header)''')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listconfigfiles(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listconfigfiles(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--sandbox', action='store_true')
    arg_parser.add_argument('-l', '--local', action='store_true')
    arg_parser.add_argument('-c', '--central', action='store_true')
    arg_parser.add_argument('-q', '--quiet', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not options.sandbox and not options.local and not options.central:
        logging.debug("No sandbox/local/central option specified, listing ALL")
        options.sandbox = True
        options.local = True
        options.central = True

    if not args:
        self.help_system_listconfigfiles()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        try:
            # Pass 0 for system-sandbox files
            # Pass 1 for locally managed or centrally managed
            files = self.client.system.config.listFiles(self.session,
                                                        system_id, 0)
            files += self.client.system.config.listFiles(self.session,
                                                         system_id, 1)
        except xmlrpclib.Fault:
            logging.warning('%s does not support configuration channels' %
                            system)
            continue

        # For system sandbox or locally managed files, there is no
        # channel_label so we add a descriptive label for these files
        toprint = []
        for f in files:
            if f['channel_type']['label'] == 'server_import':
                f['channel_label'] = "system_sandbox"
                if options.sandbox:
                    toprint.append(f)

            elif f['channel_type']['label'] == 'local_override':
                f['channel_label'] = "locally_managed"
                if options.local:
                    toprint.append(f)

            elif f['channel_type']['label'] == 'normal':
                if options.central:
                    toprint.append(f)

            else:
                logging.error("Error, unexpected channel type label %s" %
                              f['channel_type']['label'])
                return

        self.print_configfiles(options.quiet, toprint)

####################


def help_system_addconfigfile(self):
    print('system_addconfigfile: Create a configuration file')
    print('Note this is only for system sandbox or locally-managed files')
    print('Centrally managed files should be created via configchannel_addfile')
    print('''usage: system_addconfigfile [SYSTEM] [options]

options:
  -S/--sandbox : list only system-sandbox files
  -L/--local   : list only locally managed files
  -p PATH
  -r REVISION
  -o OWNER [default: root]
  -g GROUP [default: root]
  -m MODE [defualt: 0644]
  -x SELINUX_CONTEXT
  -d path is a directory
  -s path is a symlink
  -b path is a binary (or other file which needs base64 encoding)
  -t SYMLINK_TARGET
  -f local path to file contents

  Note re binary/base64: Some text files, notably those containing trailing
  newlines, those containing ASCII escape characters (or other charaters not
  allowed in XML) need to be sent as binary (-b).  Some effort is made to auto-
  detect files which require this, but you may need to explicitly specify.
''')


def complete_system_addconfigfile(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_addconfigfile(self, args, update_path=''):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-S', '--sandbox', action='store_true')
    arg_parser.add_argument('-L', '--local', action='store_true')
    arg_parser.add_argument('-p', '--path')
    arg_parser.add_argument('-o', '--owner')
    arg_parser.add_argument('-g', '--group')
    arg_parser.add_argument('-m', '--mode')
    arg_parser.add_argument('-x', '--selinux-ctx')
    arg_parser.add_argument('-t', '--target-path')
    arg_parser.add_argument('-f', '--file')
    arg_parser.add_argument('-r', '--revision')
    arg_parser.add_argument('-s', '--symlink', action='store_true')
    arg_parser.add_argument('-b', '--binary', action='store_true')
    arg_parser.add_argument('-d', '--directory', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    file_info = None

    # the system name can be passed in
    if args:
        options.system = args[0]

    interactive = is_interactive(options)
    if interactive:
        if not options.system:
            while True:
                print('Systems')
                print('----------------------')
                print('\n'.join(sorted(self.do_system_list('', True))))
                print('')

                options.system = prompt_user('Select:', noblank=True)

                # ensure the user enters a valid system
                if options.system in self.do_system_list('', True):
                    break
                else:
                    print('')
                    logging.warning('%s is not a valid system' %
                                    options.system)
                    print('')

        if update_path:
            options.path = update_path
        else:
            options.path = prompt_user('Path:', noblank=True)

        while not options.local and not options.sandbox:
            answer = prompt_user('System-Sandbox or Locally-Managed? [S/L]:')
            if re.match('L', answer, re.I):
                options.local = True
                localopt = 1
            elif re.match('S', answer, re.I):
                options.sandbox = True
                localopt = 0

    # Set the int variable (required by the API calls) for sandbox/local
    localopt = 0
    if options.local:
        logging.debug("Selected locally-managed")
        localopt = 1
    elif options.sandbox:
        logging.debug("Selected system-sandbox")
    else:
        logging.error("Must choose system-sandbox or locally-managed option")
        self.help_system_addconfigfile()
        return

    if not options.system:
        logging.error("Must provide system")
        self.help_system_addconfigfile()
        return

    system_id = self.get_system_id(options.system)
    logging.debug("Got ID %s for system %s" % (system_id, options.system))

    # check if this file already exists
    try:
        file_info = self.client.system.config.lookupFileInfo(self.session,
                                                             system_id, [options.path], localopt)
        if file_info:
            logging.debug("Found existing file_info %s" % file_info)
    except xmlrpclib.Fault:
        logging.debug("No existing file information found for %s" %
                      options.path)

    file_info = self.configfile_getinfo(args, options, file_info, interactive)

    if self.user_confirm():
        if options.symlink:
            self.client.system.config.createOrUpdateSymlink(self.session,
                                                            system_id, options.path, file_info, localopt)
        else:
            self.client.system.config.createOrUpdatePath(self.session,
                                                         system_id, options.path, options.directory, file_info,
                                                         localopt)

####################


def help_system_addconfigchannels(self):
    print('system_addconfigchannels: Add config channels to a system')
    print('''usage: system_addconfigchannels <SYSTEMS> <CHANNEL ...> [options]

options:
  -t add channels to the top of the list
  -b add channels to the bottom of the list''')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_addconfigchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.do_configchannel_list('', True),
                             text)


def do_system_addconfigchannels(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-t', '--top', action='store_true')
    arg_parser.add_argument('-b', '--bottom', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_addconfigchannels()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    channels = args

    if is_interactive(options):
        answer = prompt_user('Add to top or bottom? [T/b]:')
        if re.match('b', answer, re.I):
            options.top = False
        else:
            options.top = True
    else:
        if options.bottom:
            options.top = False
        else:
            options.top = True

    system_ids = [self.get_system_id(s) for s in systems]

    self.client.system.config.addChannels(self.session,
                                          system_ids,
                                          channels,
                                          options.top)

####################


def help_system_removeconfigchannels(self):
    print('system_removeconfigchannels: Remove config channels from a system')
    print('usage: system_removeconfigchannels <SYSTEMS> <CHANNEL ...>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_removeconfigchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.do_configchannel_list('', True),
                             text)


def do_system_removeconfigchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_removeconfigchannels()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    channels = args

    system_ids = [self.get_system_id(s) for s in systems]

    self.client.system.config.removeChannels(self.session,
                                             system_ids,
                                             channels)

####################


def help_system_setconfigchannelorder(self):
    print('system_setconfigchannelorder: Set the ranked order of configuration channels')
    print('usage: system_setconfigchannelorder <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_setconfigchannelorder(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_setconfigchannelorder(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_setconfigchannelorder()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args.pop(0))

    # get the current configuration channels from the first system
    # in the list
    system_id = self.get_system_id(systems[0])
    new_channels = self.client.system.config.listChannels(self.session,
                                                          system_id)
    new_channels = [c.get('label') for c in new_channels]

    # call an interface for the user to make selections
    all_channels = self.do_configchannel_list('', True)
    new_channels = config_channel_order(all_channels, new_channels)

    print('')
    print('New Configuration Channels')
    print('--------------------------')
    for i, new_channel in enumerate(new_channels, 1):
        print('[%i] %s' % (i, new_channel))

    if not self.user_confirm():
        return

    system_ids = [self.get_system_id(s) for s in systems]

    self.client.system.config.setChannels(self.session,
                                          system_ids,
                                          new_channels)

####################


def help_system_deployconfigfiles(self):
    print('system_deployconfigfiles: Deploy all configuration files for a system')
    print('''usage: system_deployconfigfiles <SYSTEMS> [options]

options:
    -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_deployconfigfiles(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_deployconfigfiles(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_deployconfigfiles()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    if not systems:
        return

    print('')
    print('Start Time: %s' % options.start_time)
    print('')
    print('Systems')
    print('-------')
    print('\n'.join(sorted(systems)))

    message = 'Deploy ALL configuration files to these systems [y/N]:'
    if not self.user_confirm(message):
        return

    system_ids = [self.get_system_id(s) for s in systems]

    self.client.system.config.deployAll(self.session,
                                        system_ids,
                                        options.start_time)

    logging.info('Scheduled deployment for %i system(s)' % len(system_ids))

####################


def help_system_delete(self):
    print('system_delete: Delete a system profile')
    print('''usage: system_delete [options] <SYSTEMS>

    options:
          -c TYPE - Possible values:
             *  'FAIL_ON_CLEANUP_ERR' - fail in case of cleanup error,
             *  'NO_CLEANUP' - do not cleanup, just delete,
             *  'FORCE_DELETE' - Try cleanup first but delete server anyway in case of error
    ''')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_delete(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_delete(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--cleanuptype', default='NO_CLEANUP',
            choices=['FAIL_ON_CLEANUP_ERR', 'NO_CLEANUP', 'FORCE_DELETE'])

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_delete()
        return

    system_ids = []

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    # get the system ID for each system
    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        system_ids.append(system_id)

    if not system_ids:
        logging.warning('No systems to delete')
        return

    # make the column the right size
    colsize = max_length([self.get_system_name(s) for s in system_ids])
    if colsize < 7:
        colsize = 7

    print('%s  System ID' % 'Profile'.ljust(colsize))
    print('%s  ---------' % ('-' * colsize))

    # print(a summary for the user)
    for system_id in system_ids:
        print('%s  %i' %
              (self.get_system_name(system_id).ljust(colsize), system_id))

    if not self.user_confirm('Delete these systems [y/N]:'):
        return

    self.client.system.deleteSystems(self.session, system_ids, options.cleanuptype)

    logging.info('%i system(s) scheduled for removal', len(system_ids))

    # regenerate the system name cache
    self.generate_system_cache(True, delay=1)

    # remove these systems from the SSM and update the cache
    all(self.ssm.pop(system_name) for system_name in list(systems))
    save_cache(self.ssm_cache_file, self.ssm)


####################


def help_system_lock(self):
    print('system_lock: Lock a system')
    print('usage: system_lock <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_lock(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_lock(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_lock()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.setLockStatus(self.session, system_id, True)

####################


def help_system_unlock(self):
    print('system_unlock: Unlock a system')
    print('usage: system_unlock <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_unlock(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_unlock(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_unlock()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.setLockStatus(self.session, system_id, False)

####################


def help_system_rename(self):
    print('system_rename: Rename a system profile')
    print('usage: system_rename OLDNAME NEWNAME')


def complete_system_rename(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return tab_completer(self.get_system_names(), text)


def do_system_rename(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_system_rename()
        return

    (old_name, new_name) = args

    system_id = self.get_system_id(old_name)
    if not system_id:
        return

    print('%s (%s) -> %s' % (old_name, system_id, new_name))
    if not self.user_confirm():
        return

    self.client.system.setProfileName(self.session,
                                      system_id,
                                      new_name)

    # regenerate the cache of systems
    self.generate_system_cache(True)

    # update the SSM
    if old_name in self.ssm:
        self.ssm.remove(old_name)
        self.ssm.append(new_name)

####################


def help_system_listcustomvalues(self):
    print('system_listcustomvalues: List the custom values for a system')
    print('usage: system_listcustomvalues <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listcustomvalues(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listcustomvalues(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listcustomvalues()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in systems:
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        system_id = self.get_system_id(system)
        if not system_id:
            continue

        values = self.client.system.getCustomValues(self.session,
                                                    system_id)

        for v in values:
            print('%s = %s' % (v, values[v]))

####################


def help_system_addcustomvalue(self):
    print('system_addcustomvalue: Set a custom value for a system')
    print('usage: system_addcustomvalue KEY VALUE <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_addcustomvalue(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_custominfo_listkeys('', True), text)
    elif len(parts) >= 4:
        return self.tab_complete_systems(text)


def do_system_addcustomvalue(self, args):
    if not isinstance(args, list):
        arg_parser = get_argument_parser()

        (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_system_addcustomvalue()
        return

    key = args[0]
    value = args[1]

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args[2:])

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.setCustomValues(self.session,
                                           system_id,
                                           {key: value})

####################


def help_system_updatecustomvalue(self):
    print('system_updatecustomvalue: Update a custom value for a system')
    print('usage: system_updatecustomvalue KEY VALUE <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_updatecustomvalue(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_custominfo_listkeys('', True), text)
    elif len(parts) >= 4:
        return self.tab_complete_systems(text)


def do_system_updatecustomvalue(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 3:
        self.help_system_updatecustomvalue()
        return

    return self.do_system_addcustomvalue(args)

####################


def help_system_removecustomvalues(self):
    print('system_removecustomvalues: Remove a custom value for a system')
    print('usage: system_removecustomvalues <SYSTEMS> <KEY ...>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_removecustomvalues(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) == 3:
        return tab_completer(self.do_custominfo_listkeys('', True),
                             text)


def do_system_removecustomvalues(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_removecustomvalues()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    keys = args[1:]

    if not self.user_confirm('Delete these values [y/N]:'):
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.deleteCustomValues(self.session,
                                              system_id,
                                              keys)

####################


def help_system_addnote(self):
    print('system_addnote: Set a note for a system')
    print('''usage: system_addnote <SYSTEM> [options]

options:
  -s SUBJECT
  -b BODY''')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_addnote(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_addnote(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--subject')
    arg_parser.add_argument('-b', '--body')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 1:
        self.help_system_addnote()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    if is_interactive(options):
        options.subject = prompt_user('Subject of the Note:', noblank=True)

        message = 'Note Body (ctrl-D to finish):'
        options.body = prompt_user(message, noblank=True, multiline=True)
    else:
        if not options.subject:
            logging.error('A subject is required')
            return

        if not options.body:
            logging.error('A body is required')
            return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.addNote(self.session,
                                   system_id,
                                   options.subject,
                                   options.body)

####################


def help_system_deletenotes(self):
    print('system_deletenotes: Delete notes from a system')
    print('usage: system_deletenotes <SYSTEM> <ID|*>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_deletenotes(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_deletenotes(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listnotes()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    note_ids = args

    if not args:
        logging.warning('No notes to delete')
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if '.*' in note_ids:
            self.client.system.deleteNotes(self.session, system_id)
        else:
            for note_id in note_ids:
                try:
                    note_id = int(note_id)
                except ValueError:
                    logging.warning('%s is not a valid note ID' % note_id)
                    continue

                # deleteNote does not throw an exception
                self.client.system.deleteNote(self.session, system_id, note_id)

####################


def help_system_listnotes(self):
    print('system_listnotes: List the available notes for a system')
    print('usage: system_listnotes <SYSTEM>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listnotes(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listnotes(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listnotes()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in sorted(systems):
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        system_id = self.get_system_id(system)
        if not system_id:
            continue

        notes = self.client.system.listNotes(self.session, system_id)

        for n in notes:
            print('%d. %s (%s)' % (n['id'], n['subject'], n['creator']))
            print(n['note'])
            print('')

####################

####################


def help_system_listfqdns(self):
    print('system_listfqdns: List the associated FQDNs for a system')
    print('usage: system_listfqdns <SYSTEM>')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listfqdns(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listfqdns(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not len(args):
        self.help_system_listfqdns()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in sorted(systems):
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        system_id = self.get_system_id(system)
        if not system_id:
            continue

        fqdns = self.client.system.listFqdns(self.session, system_id)

        for f in fqdns:
            print(f)

####################

def help_system_setbasechannel(self):
    print("system_setbasechannel: Set a system's base software channel")
    print('usage: system_setbasechannel <SYSTEMS> CHANNEL')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_setbasechannel(self, text, line, beg, end):
    if len(line.split(' ')) == 2:
        return self.tab_complete_systems(text)
    elif len(line.split(' ')) == 3:
        return tab_completer(self.list_base_channels(), text)


def do_system_setbasechannel(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_system_setbasechannel()
        return

    new_channel = args.pop()

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        old = self.client.system.getSubscribedBaseChannel(self.session,
                                                          system_id)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('System:           %s' % system)
        print('Old Base Channel: %s' % old.get('label'))
        print('New Base Channel: %s' % new_channel)

    if not self.user_confirm():
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.setBaseChannel(self.session,
                                          system_id,
                                          new_channel)

####################

def help_system_schedulechangechannels(self):
    print("system_schedulechangechannels: Schedule changing a system's software channels")
    print('''usage: system_setbasechannel <SYSTEMS> [options]

options:
  -b BASE_CHANNEL base channel label
  -c CHILD_CHANNEL child channel labels (allowed multiple times)
  -s START_TIME time defaults to now''')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_schedulechangechannels(self, text, line, beg, end):

    if len(line.split(' ')) == 2:
        return self.tab_complete_systems(text)

def do_system_schedulechangechannels(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-b', '--base')
    arg_parser.add_argument('-c', '--child', action='append', default=[])
    arg_parser.add_argument('-s', '--start-time', action='store')

    (args, options) = parse_command_arguments(args, arg_parser)
    # import pdb;
    # pdb.set_trace()
    if len(args) < 1:
        self.help_system_schedulechangechannels()
        return

    if not options.base:
        logging.error('A base channel is required')
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    if not options.start_time:
        options.start_time = parse_time_input('now')
    else:
        options.start_time = parse_time_input(options.start_time)

    baseChannel = options.base
    childChannels = options.child or []

    add_separator = False

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        oldBase = self.client.system.getSubscribedBaseChannel(self.session,
                                                          system_id)

        oldKids = self.client.system.listSubscribedChildChannels(self.session,
                                                       system_id)
        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('System:           %s' % system)
        print('Old Base Channel: %s' % oldBase.get('label'))
        print('Old Child Channels: %s' % ', '.join([k.get('label') for k in oldKids]))
        print('New Base Channel: %s' % baseChannel)
        print('New Child channels %s' % ', '.join(childChannels))

    if not self.user_confirm():
        return

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        actionId = self.client.system.scheduleChangeChannels(self.session,
                                          system_id,
                                          baseChannel,
                                          childChannels,
                                          options.start_time)
        print('Scheduled action id: %s' % actionId)

####################

def help_system_listbasechannel(self):
    print('system_listbasechannel: List the base channel for a system')
    print('usage: system_listbasechannel <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listbasechannel(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listbasechannel(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listbasechannel()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        channel = \
            self.client.system.getSubscribedBaseChannel(self.session,
                                                        system_id)

        print(channel.get('label'))

####################


def help_system_listchildchannels(self):
    print('system_listchildchannels: List the child channels for a system')
    print('usage: system_listchildchannels <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listchildchannels(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listchildchannels(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listchildchannels()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        channels = \
            self.client.system.listSubscribedChildChannels(self.session,
                                                           system_id)

        print('\n'.join(sorted([c.get('label') for c in channels])))

####################


def help_system_addchildchannels(self):
    print("system_addchildchannels: Add child channels to a system")
    print('usage: system_addchildchannels <SYSTEMS> <CHANNEL ...>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_addchildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.list_child_channels(), text)


def do_system_addchildchannels(self, args):
    self.manipulate_child_channels(args)

####################


def help_system_listcrashedsystems(self):
    print("system_listcrashedsystems: List all systems that have experienced a crash and reported by spacewalk-abrt")
    print('usage: system_listcrashedsystems')
    print('')


def do_system_listcrashedsystems(self, args):
    print('')
    print('Count | System ID | Profile Name')
    print('--------------------------------')
    res = self.client.system.listUserSystems(self.session)
    for s in res:
        res_crash = self.client.system.crash.listSystemCrashes(self.session, s['id'])
        if res_crash:
            print("%d : %s : %s" % (len(res_crash), s['id'], s['name']))

######


def help_system_deletecrashes(self):
    print('system_deletecrashes: Delete crashes reported by spacewalk-abrt.')
    print('usage: Delete all crashes for all systems    : system_deletecrashes [--verbose]')
    print('usage: Delete all crashes for a single system: system_deletecrashes -i sys_id [--verbose]')
    print('usage: Delete a single crash record          : system_deletecrashes -c crash_id [--verbose]')
    print('')


def print_msg(string_msg, flag_verbose):
    if flag_verbose:
        print(string_msg)


def do_system_deletecrashes(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-i', '--sysid')
    arg_parser.add_argument('-c', '--crashid')
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if options.crashid:
        print_msg("Deleting crash with id %s." % options.crashid, options.verbose)
        self.client.system.crash.deleteCrash(self.session, int(options.crashid))
        return

    sys_id = []
    if options.sysid:
        sys_id.append(options.sysid)
        prompt_string = "Deleting all crashes from system with systemid %s [y/N]:" % options.sysid
    else:  # all systems
        prompt_string = 'Deleting all crashes from all systems [y/N]:'
        systems = self.client.system.listUserSystems(self.session)
        for s in systems:
            sys_id.append(s['id'])

    confirm = prompt_user(prompt_string)
    if re.match('n', confirm, re.I):
        return

    for s_id in sys_id:
        list_crash = self.client.system.crash.listSystemCrashes(self.session, int(s_id))
        for crash in list_crash:
            print_msg("Deleting crash with id %s from system %s." % (crash['id'], s_id), options.verbose)
            self.client.system.crash.deleteCrash(self.session, int(crash['id']))

#######


def help_system_listcrashesbysystem(self):
    print('system_listcrashesbysystem: List all reported crashes for a system.')
    print('usage: system_listcrashesbysystem -i sys_id')
    print('')


def do_system_listcrashesbysystem(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-i', '--sysid')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not options.sysid:
        print("# System id must be provided.")
        print("usage: system_listcrashesbysystem -i sys_id")
        return

    l_crashes = self.client.system.crash.listSystemCrashes(self.session, int(options.sysid))
    print('')
    print('Crash ID | Crash Name')
    print('---------------------')

    for cr in l_crashes:
        print("| %s  | %s" % (cr['id'], cr['crash']))


#######

def help_system_getcrashfiles(self):
    print('system_getcrashfiles: Download all files for a crash record.')
    print('usage: system_getcrashfiles -c crash_id [--verbose]')
    print('usage: system_getcrashfiles -c crash_id [--dest_folder=/tmp/crash_files] [--verbose]')
    print('')


def do_system_getcrashfiles(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--crashid')
    arg_parser.add_argument('-d', '--dest_folder')
    arg_parser.add_argument('-v', '--verbose', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not options.crashid:
        print("# Crash id must be provided.")
        print("usage: system_getcrashfiles -c crash_id [--dest_folder=/tmp/crash_files] [--verbose]")
        return

    if not options.verbose:
        options.verbose = "&>/dev/null"

    # create date stamp
    date_stamp = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')

    if not options.dest_folder:
        options.dest_folder = "files_for_" + "crashid_" + options.crashid + "_" + date_stamp
    else:
        options.dest_folder += "_" + date_stamp

    # create folder
    os.system("mkdir %s" % options.dest_folder)
    l_files = self.client.system.crash.listSystemCrashFiles(self.session, int(options.crashid))

    for f in l_files:
        file_url = self.client.system.crash.getCrashFileUrl(self.session, f['id'])
        os.system("wget  --directory-prefix=%s --tries=1 --no-check-certificate %s %s" % (
            options.dest_folder,
            file_url,
            options.verbose))

    print('')
    print("# All files we downloaded to %s." % options.dest_folder)
    print('')

####################


def help_system_removechildchannels(self):
    print("system_removechildchannels: Remove child channels from a system")
    print('usage: system_removechildchannels <SYSTEMS> <CHANNEL ...>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_removechildchannels(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return tab_completer(self.list_child_channels(), text)


def do_system_removechildchannels(self, args):
    self.manipulate_child_channels(args, True)

####################


def help_system_details(self):
    print('system_details: Show the details of a system profile')
    print('usage: system_details <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_details(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_details(self, args, short=False):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_details()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        last_checkin = \
            self.client.system.getName(self.session,
                                       system_id).get('last_checkin')

        details = self.client.system.getDetails(self.session, system_id)

        if self.check_api_version('10.16'):
            uuid = self.client.system.getUuid(self.session, system_id)
        else:
            uuid = None

        registered = self.client.system.getRegistrationDate(self.session,
                                                            system_id)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Name:          %s' % details.get('profile_name'))
        print('System ID:     %i' % system_id)

        if uuid:
            print('UUID:          %s' % uuid)

        print('Locked:        %s' % details.get('lock_status'))
        print('Registered:    %s' % registered)
        print('Last Checkin:  %s' % last_checkin)
        print('OSA Status:    %s' % details.get('osa_status'))
        print('Last Boot:     %s' % details.get('last_boot'))
        if 'contact_method' in details:
            print('Contact Method:%s' % details.get('contact_method'))

        # only print(basic information if requested)
        if short:
            continue

        network = self.client.system.getNetwork(self.session, system_id)

        entitlements = self.client.system.getEntitlements(self.session,
                                                          system_id)

        base_channel = \
            self.client.system.getSubscribedBaseChannel(self.session,
                                                        system_id)

        child_channels = \
            self.client.system.listSubscribedChildChannels(self.session,
                                                           system_id)

        groups = self.client.system.listGroups(self.session,
                                               system_id)

        kernel = self.client.system.getRunningKernel(self.session,
                                                     system_id)

        keys = self.client.system.listActivationKeys(self.session,
                                                     system_id)

        ranked_config_channels = []

        try:
            config_channels = \
                self.client.system.config.listChannels(self.session, system_id)
        except xmlrpclib.Fault as exc:
            # 10003 - unsupported operation
            if exc.faultCode == 10003:
                logging.debug(exc.faultString)
            else:
                logging.warning(exc.faultString)
        else:
            for channel in config_channels:
                ranked_config_channels.append(channel.get('label'))

        print('')
        print('Hostname:      %s' % network.get('hostname'))
        print('IP Address:    %s' % network.get('ip'))
        print('Kernel:        %s' % kernel)

        if keys:
            print('')
            print('Activation Keys')
            print('---------------')
            print('\n'.join(sorted(keys)))

        print('')
        print('Software Channels')
        print('-----------------')
        print(base_channel.get('label'))

        for channel in child_channels:
            print('  |-- %s' % channel.get('label'))

        if ranked_config_channels:
            print('')
            print('Configuration Channels')
            print('----------------------')
            print('\n'.join(ranked_config_channels))

        print('')
        print('Entitlements')
        print('------------')
        print('\n'.join(sorted(entitlements)))

        if groups:
            print('')
            print('System Groups')
            print('-------------')
            for group in groups:
                if group.get('subscribed') == 1:
                    print(group.get('system_group_name'))

####################


def help_system_listerrata(self):
    print('system_listerrata: List available errata for a system')
    print('usage: system_listerrata <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listerrata(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listerrata(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listerrata()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)
            print('')

        errata = self.client.system.getRelevantErrata(self.session,
                                                      system_id)

        print_errata_list(errata)

####################


def help_system_applyerrata(self):
    print('system_applyerrata: Apply errata to a system')
    print('''usage: system_applyerrata [options] <SYSTEMS>
[ERRATA|search:XXX ...]

options:
  -s START_TIME''')
    print('')
    print(self.HELP_TIME_OPTS)
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_applyerrata(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return self.tab_complete_errata(text)


def do_system_applyerrata(self, args):
    # this is really just an entry point to do_errata_apply
    # and the whole parsing of the start time needed is done
    # there; here we only make sure we accept this option
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_applyerrata()
        return

    # use the systems applyed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args.pop(0))

    # allow globbing and searching of errata
    errata_list = self.expand_errata(args)

    if not errata_list or not systems:
        return

    # reconstruct options so we can pass them to do_errata_apply
    opts = []
    if options.start_time:
        opts.append('-s ' + options.start_time)

    return self.do_errata_apply(' '.join(opts + errata_list), systems)

####################


def help_system_listevents(self):
    print('system_listevents: List the event history for a system')
    print('usage: system_listevents <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listevents(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listevents(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listevents()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    add_separator = False

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        events = self.client.system.getEventHistory(self.session, system_id)

        for e in events:
            print('')
            print('Summary:   %s' % e.get('summary'))
            print('Completed: %s' % e.get('completed'))
            print('Details:   %s' % e.get('details'))

####################


def help_system_listentitlements(self):
    print('system_listentitlements: List the entitlements for a system')
    print('usage: system_listentitlements <SYSTEMS>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_listentitlements(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_listentitlements(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_listentitlements()
        return

    add_separator = False

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        if len(systems) > 1:
            print('System: %s' % system)

        entitlements = self.client.system.getEntitlements(self.session,
                                                          system_id)

        print('\n'.join(sorted(entitlements)))

####################


def help_system_addentitlements(self):
    print('system_addentitlements: Add entitlements to a system')
    print('usage: system_addentitlements <SYSTEMS> ENTITLEMENT')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_addentitlements(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)

    return tab_completer(self.ENTITLEMENTS, text)


def do_system_addentitlements(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_addentitlements()
        return

    entitlement = args.pop()

    for e in self.ENTITLEMENTS:
        if re.match(entitlement, e, re.I):
            entitlement = e
            break

    # use the systems applyed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.addEntitlements(self.session,
                                           system_id,
                                           [entitlement])

####################


def help_system_removeentitlement(self):
    print('system_removeentitlement: Remove an entitlement from a system')
    print('usage: system_removeentitlement <SYSTEMS> ENTITLEMENT')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_removeentitlement(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)

    return tab_completer(self.ENTITLEMENTS, text)


def do_system_removeentitlement(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_removeentitlement()
        return

    entitlement = args.pop()

    for e in self.ENTITLEMENTS:
        if re.match(entitlement, e, re.I):
            entitlement = e
            break

    # use the systems applyed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.removeEntitlements(self.session,
                                              system_id,
                                              [entitlement])

####################


def help_system_listpackageprofiles(self):
    print('system_listpackageprofiles: List all package profiles')
    print('usage: system_listpackageprofiles')


def do_system_listpackageprofiles(self, args, doreturn=False):
    profiles = self.client.system.listPackageProfiles(self.session)
    profiles = [p.get('name') for p in profiles]

    if doreturn:
        return profiles
    else:
        if profiles:
            print('\n'.join(sorted(profiles)))

####################


def help_system_deletepackageprofile(self):
    print('system_deletepackageprofile: Delete a package profile')
    print('usage: system_deletepackageprofile PROFILE')


def complete_system_deletepackageprofile(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return self.tab_complete_systems(
            self.do_system_listpackageprofiles('', True), text)


def do_system_deletepackageprofile(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_deletepackageprofile()
        return

    label = args[0]

    if not self.user_confirm('Delete this profile [y/N]:'):
        return

    all_profiles = self.client.system.listPackageProfiles(self.session)

    profile_id = 0
    for profile in all_profiles:
        if label == profile.get('name'):
            profile_id = profile.get('id')

    if not profile_id:
        logging.warning('%s is not a valid profile' % label)
        return

    self.client.system.deletePackageProfile(self.session, profile_id)

####################


def help_system_createpackageprofile(self):
    print('system_createpackageprofile: Create a package profile')
    print('''usage: system_createpackageprofile SYSTEM [options]

options:
  -n NAME
  -d DESCRIPTION''')


def complete_system_createpackageprofile(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)


def do_system_createpackageprofile(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-n', '--name')
    arg_parser.add_argument('-d', '--description')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_system_createpackageprofile()
        return

    system_id = self.get_system_id(args[0])
    if not system_id:
        return

    if is_interactive(options):
        options.name = prompt_user('Profile Label:', noblank=True)
        options.description = prompt_user('Description:', multiline=True)
    else:
        if not options.name:
            logging.error('A profile name is required')
            return

        if not options.description:
            logging.error('A profile description is required')
            return

    self.client.system.createPackageProfile(self.session,
                                            system_id,
                                            options.name,
                                            options.description)

    logging.info("Created package profile '%s'" % options.name)

####################


def help_system_comparepackageprofile(self):
    print('system_comparepackageprofile: Compare a system against a package profile')
    print('usage: system_comparepackageprofile <SYSTEMS> PROFILE')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_comparepackageprofile(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    elif len(parts) > 2:
        return self.tab_complete_systems(
            self.do_system_listpackageprofiles('', True), parts[-1])


def do_system_comparepackageprofile(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_comparepackageprofile()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
        args.pop(0)
    else:
        systems = self.expand_systems(args[:-1])

    profile = args[-1]

    add_separator = False

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        results = self.client.system.comparePackageProfile(self.session,
                                                           system_id,
                                                           profile)

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('%s:' % system)
        self.print_package_comparison(results)

####################


def help_system_comparepackages(self):
    print('system_comparepackages: Compare the packages between two systems')
    print('usage: system_comparepackages SOME_SYSTEM ANOTHER_SYSTEM')


def complete_system_comparepackages(self, text, line, beg, end):
    return tab_completer(self.get_system_names(), text)


def do_system_comparepackages(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_system_comparepackages()
        return

    this_system = self.get_system_id(args[0])
    other_system = self.get_system_id(args[1])

    results = self.client.system.comparePackages(self.session,
                                                 this_system,
                                                 other_system)

    self.print_package_comparison(results)

####################


def help_system_syncpackages(self):
    print('system_syncpackages: Sync packages between two systems')
    print('''usage: system_syncpackages SOURCE TARGET [options]

options:
    -s START_TIME''')
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_syncpackages(self, text, line, beg, end):
    return tab_completer(self.get_system_names(), text)


def do_system_syncpackages(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_system_syncpackages()
        return

    (source, target) = args

    source_id = self.get_system_id(source)
    target_id = self.get_system_id(target)

    if not source_id or not target_id:
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # show a comparison and ask for confirmation
    self.do_system_comparepackages('%s %s' % (source_id, target_id))

    print('')
    print('Start Time: %s' % options.start_time)

    if not self.user_confirm('Sync packages [y/N]:'):
        return

    # get package IDs
    packages = self.client.system.listPackages(self.session, source_id)

    package_names = build_package_names(packages)

    package_ids = []

    for name in package_names:
        p_ids = self.get_package_id(name)

        # filter out invalid package IDs
        if p_ids:
            package_ids += p_ids

    self.client.system.scheduleSyncPackagesWithSystem(self.session,
                                                      target_id,
                                                      source_id,
                                                      package_ids,
                                                      options.start_time)
####################


def filter_latest_packages(pkglist, version_key='version',
                           release_key='release', epoch_key='epoch'):
    # Returns a dict, indexed by a compound (tuple) key based on
    # arch and name, so we can store the latest version of each package
    # for each arch.  This approach avoids nested loops :)
    latest = {}
    for p in pkglist:
        if 'arch_label' in p:
            tuplekey = p['name'], p['arch_label']
        elif 'arch' in p:
            # Fixup arch==AMD64 which is returned for some reason
            p['arch'] = re.sub('AMD64', 'x86_64', p['arch'])
            tuplekey = p['name'], p['arch']
        else:
            logging.error("Failed to filter package list, package %s" % p
                          + "found with no arch or arch_label")
            return None
        if not tuplekey in latest:
            latest[tuplekey] = p
        else:
            # Already have this package, is p newer?
            if p == latest_pkg(p, latest[tuplekey], version_key, release_key, epoch_key):
                latest[tuplekey] = p

    return latest


def print_comparison_withchannel(self, channelnewer, systemnewer,
                                 channelmissing, channel_latest):

    # Figure out correct indentation to allow pretty table output
    results = channelnewer + systemnewer + channelmissing

    tmp_names = []
    tmp_system = []
    tmp_channel = []
    for item in results:
        name_string = "%(name)s.%(arch)s" % item
        tmp_names.append(name_string)
        # Create two version-string lists, one for the version in the results
        # list, and another with the version string from the channel_latest
        # dict, if the channel contains a matching package
        version_string = "%(version)s-%(release)s" % item
        tmp_system.append(version_string)
        key = item['name'], item['arch']
        if key in channel_latest:
            version_string = "%(version)s-%(release)s" % channel_latest[key]
            tmp_channel.append(version_string)

    max_name = max_length(tmp_names, minimum=7)
    max_system = max_length(tmp_system, minimum=11)
    max_channel = max_length(tmp_channel, minimum=15)
    max_comparison = 25

    # print(headers)
    print('%s  %s  %s  %s' % (
        'Package'.ljust(max_name),
        'System Version'.ljust(max_system),
        'Channel Version'.ljust(max_channel),
        'Difference'.ljust(max_comparison)))

    print('%s  %s  %s  %s' % (
        '-' * max_name,
        '-' * max_system,
        '-' * max_channel,
        '-' * max_comparison))

    # Then print(the packages)
    for item in channelnewer:
        name_string = "%(name)s.%(arch)s" % item
        version_string = "%(version)s-%(release)s" % item
        key = item['name'], item['arch']
        if key in channel_latest:
            channel_version = "%(version)s-%(release)s" % channel_latest[key]
        else:
            channel_version = '-'
        print('%s  %s  %s  %s' % (
            name_string.ljust(max_name),
            version_string.ljust(max_system),
            channel_version.ljust(max_channel),
            "Channel_newer_than_system".ljust(max_comparison)))
    for item in systemnewer:
        name_string = "%(name)s.%(arch)s" % item
        version_string = "%(version)s-%(release)s" % item
        key = item['name'], item['arch']
        if key in channel_latest:
            channel_version = "%(version)s-%(release)s" % channel_latest[key]
        else:
            channel_version = '-'
        print('%s  %s  %s  %s' % (
            name_string.ljust(max_name),
            version_string.ljust(max_system),
            channel_version.ljust(max_channel),
            "System_newer_than_channel".ljust(max_comparison)))
    for item in channelmissing:
        name_string = "%(name)s.%(arch)s" % item
        version_string = "%(version)s-%(release)s" % item
        channel_version = '-'
        print('%s  %s  %s  %s' % (
            name_string.ljust(max_name),
            version_string.ljust(max_system),
            channel_version.ljust(max_channel),
            "Missing_in_channel".ljust(max_comparison)))


def help_system_comparewithchannel(self):
    print('system_comparewithchannel: Compare the installed packages on a')
    print('                           system with those in the channels it is')
    print('                           registerd to, or optionally some other')
    print('                           channel')
    print('usage: system_comparewithchannel <SYSTEMS> [options]')
    print('options:')
    print('         -c/--channel : Specific channel to compare against,')
    print('                        default is those subscribed to, including')
    print('                        child channels')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_comparewithchannel(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_comparewithchannel(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-c', '--channel')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_comparewithchannel()
        return

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    channel_latest = {}
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        instpkgs = self.client.system.listPackages(self.session,
                                                   system_id)
        logging.debug("Got %d packages installed in system %s" %
                      (len(instpkgs), system))
        # We need to filter to get only the latest installed packages,
        # because multiple versions (e.g kernel) can be installed
        packages = filter_latest_packages(instpkgs)
        logging.debug("Got latest %d packages installed in system %s" %
                      (len(packages.keys()), system))

        channels = []
        if options.channel:
            # User specified a specific channel, check it exists
            allch = self.client.channel.listSoftwareChannels(self.session)
            allch_labels = [c['label'] for c in allch]
            if not options.channel in allch_labels:
                logging.error("Specified channel does not exist")
                self.help_system_comparewithchannel()
                return
            channels = [options.channel]
            logging.debug("User specified channel %s" % options.channel)
        else:
            # No specified channel, so we create a list of all channels the
            # system is subscribed to
            basech = self.client.system.getSubscribedBaseChannel(self.session,
                                                                 system_id)
            if not basech:
                logging.error("system %s is not subscribed to any channel!"
                              % system)
                logging.error("Please subscribe to a channel, or specify a" +
                              "channel to compare with")
                return
            logging.debug("base channel %s for %s" % (basech['name'], system))
            childch = self.client.system.listSubscribedChildChannels(
                self.session, system_id)
            channels = [basech['label']]
            for c in childch:
                channels.append(c['label'])

        # Get the latest packages in each channel
        latestpkgs = {}
        for c in channels:
            if not c in channel_latest:
                logging.debug("Getting packages for channel %s" % c)
                pkgs = self.client.channel.software.listAllPackages(
                    self.session, c)
                # filter_latest_packages Returns a dict of latest packages
                # indexed by name,arch tuple, which we add to the dict-of-dict
                # channel_latest, to avoid getting the same channel data
                # multiple times when processing more than one system
                channel_latest[c] = filter_latest_packages(pkgs)
            # Merge the channel latest dicts into one latestpkgs dict
            # We handle collisions and only store the latest version
            # We do this for every channel of every system, since the mix of
            # subscribed channels may be different
            for key in channel_latest[c].keys():
                if not key in latestpkgs:
                    latestpkgs[key] = channel_latest[c][key]
                else:
                    p_newest = latest_pkg(channel_latest[c][key], latestpkgs[key])
                    latestpkgs[key] = p_newest

        if len(systems) > 1:
            print('\nSystem: %s' % system)

        # Iterate over the installed packages
        channelnewer = []
        systemnewer = []
        channelmissing = []
        for key in packages:
            syspkg = packages.get(key)
            if key in latestpkgs:
                chpkg = latestpkgs.get(key)
                newest = latest_pkg(syspkg, chpkg)
                if syspkg == newest:
                    systemnewer.append(syspkg)
                elif chpkg == newest:
                    channelnewer.append(syspkg)
            else:
                channelmissing.append(syspkg)
        self.print_comparison_withchannel(channelnewer, systemnewer,
                                          channelmissing, latestpkgs)

####################


def help_system_schedulehardwarerefresh(self):
    print('system_schedulehardwarerefresh: Schedule a hardware refresh for a system')
    print('''usage: system_schedulehardwarerefresh <SYSTEMS> [options]

options:
  -s START_TIME''')

    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_schedulehardwarerefresh(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_schedulehardwarerefresh(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_schedulehardwarerefresh()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.scheduleHardwareRefresh(self.session,
                                                   system_id,
                                                   options.start_time)

####################


def help_system_schedulepackagerefresh(self):
    print('system_schedulepackagerefresh: Schedule a software package refresh for a system')
    print('''usage: system_schedulepackagerefresh <SYSTEMS> [options])

options:
  -s START_TIME''')
    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def complete_system_schedulepackagerefresh(self, text, line, beg, end):
    return self.tab_complete_systems(text)


def do_system_schedulepackagerefresh(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_schedulepackagerefresh()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in systems:
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.schedulePackageRefresh(self.session,
                                                  system_id,
                                                  options.start_time)

####################


def help_system_show_packageversion(self):
    print('system_show_packageversion: Shows version of installed package on given system(s)')
    print('usage: system_show_packageversion <SYSTEM> <PACKAGE>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_show_packageversion(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)

    return tab_completer(self.get_package_names(), text)


def do_system_show_packageversion(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_system_show_packageversion()
        return

    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    print("Package\tVersion\tRelease\tEpoch\tArch\tSystem")
    print("==============================================")
    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        instpkgs = self.client.system.listPackages(self.session, system_id)
        searchpkg = args[1]
        for pkg in instpkgs:
            if pkg.get('name') == searchpkg:
                print("%s\t%s\t%s\t%s\t%s\t%s" % (pkg.get('name'), pkg.get('version'), pkg.get('release'),
                                                  pkg.get('epoch'), pkg.get('arch_label'), system))

####################


def help_system_setcontactmethod(self):
    print('system_setcontactmethod: Set the contact method for given system(s).')
    print('Available contact methods: ' + str(self.CONTACT_METHODS))
    print('usage: system_setcontactmethod <SYSTEMS> <CONTACT_METHOD>')
    print('')
    print(self.HELP_SYSTEM_OPTS)


def complete_system_setcontactmethod(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return self.tab_complete_systems(text)
    else:
        return tab_completer(self.CONTACT_METHODS, text)


def do_system_setcontactmethod(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_system_setcontactmethod()
        return

    contact_method = args.pop()
    details = {'contact_method': contact_method}

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    for system in sorted(systems):
        system_id = self.get_system_id(system)
        if not system_id:
            continue

        self.client.system.setDetails(self.session, system_id, details)
        ####################


def help_system_scheduleapplyconfigchannels(self):
    print("system_scheduleapplyconfigchannels: Schedule applying the assigned config channels to the System (Minion only)")
    print('''usage: scheduleapplyconfigchannels <SYSTEMS> [options]

    options:
        -s START_TIME''')
    print('')
    print(self.HELP_SYSTEM_OPTS)
    print('')
    print(self.HELP_TIME_OPTS)


def do_system_scheduleapplyconfigchannels(self, args):


    arg_parser = get_argument_parser()
    arg_parser.add_argument('-s', '--start-time')

    (args, options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_system_scheduleapplyconfigchannels()
        return

    # get the start time option
    # skip the prompt if we are running with --yes
    # use "now" if no start time was given
    if is_interactive(options) and self.options.yes != True:
        options.start_time = prompt_user('Start Time [now]:')
        options.start_time = parse_time_input(options.start_time)
    else:
        if not options.start_time:
            options.start_time = parse_time_input('now')
        else:
            options.start_time = parse_time_input(options.start_time)

    # use the systems listed in the SSM
    if re.match('ssm', args[0], re.I):
        systems = self.ssm.keys()
    else:
        systems = self.expand_systems(args)

    if not systems:
        return

    print('')
    print('Start Time: %s' % options.start_time)
    print('')
    print('Systems')
    print('-------')
    print('\n'.join(sorted(systems)))

    message = 'Schedule applying config channels to these systems [y/N]:'
    if not self.user_confirm(message):
        return

    system_ids = [self.get_system_id(s) for s in systems]

    actionId = self.client.system.config.scheduleApplyConfigChannel(self.session,
                                                                    system_ids,
                                                                    options.start_time, False)
    print('Scheduled action id: %s' % actionId)
    ####################
0707010000002B000081B40000000000000000000000015DA8415F00004AA8000000000000000000000000000000000000001E00000000spacecmd/src/spacecmd/user.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2013--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import shlex
from getpass import getpass
try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from spacecmd.utils import *


def help_user_create(self):
    print('user_create: Create an user')
    print('''usage: user_create [options])

options:
  -u USERNAME
  -f FIRST_NAME
  -l LAST_NAME
  -e EMAIL
  -p PASSWORD
  --pam enable PAM authentication''')


def do_user_create(self, args):
    arg_parser = get_argument_parser()
    arg_parser.add_argument('-u', '--username')
    arg_parser.add_argument('-f', '--first-name')
    arg_parser.add_argument('-l', '--last-name')
    arg_parser.add_argument('-e', '--email')
    arg_parser.add_argument('-p', '--password')
    arg_parser.add_argument('--pam', action='store_true')

    (args, options) = parse_command_arguments(args, arg_parser)

    if is_interactive(options):
        options.username = prompt_user('Username:', noblank=True)
        options.first_name = prompt_user('First Name:', noblank=True)
        options.last_name = prompt_user('Last Name:', noblank=True)
        options.email = prompt_user('Email:', noblank=True)
        options.pam = self.user_confirm('PAM Authentication [y/N]:',
                                        nospacer=True,
                                        integer=True,
                                        ignore_yes=True)

        options.password = ''
        while options.password == '':
            password1 = getpass('Password: ')
            password2 = getpass('Repeat Password: ')

            if password1 == password2:
                options.password = password1
            elif password1 == '':
                logging.warning('Password must be at least 5 characters')
            else:
                logging.warning("Passwords don't match")
    else:
        if not options.username:
            logging.error('A username is required')
            return

        if not options.first_name:
            logging.error('A first name is required')
            return

        if not options.last_name:
            logging.error('A last name is required')
            return

        if not options.email:
            logging.error('An email address is required')
            return

        if not options.password and not options.pam:
            logging.error('A password is required')
            return

        if options.pam:
            options.pam = 1
            # API requires a non-None password even though it's not used
            # when PAM is enabled
            if options.password:
                logging.warning("Note password field is ignored for PAM mode")
            options.password = ""
        else:
            options.pam = 0

    self.client.user.create(self.session,
                            options.username,
                            options.password,
                            options.first_name,
                            options.last_name,
                            options.email,
                            options.pam)

####################


def help_user_delete(self):
    print('user_delete: Delete an user')
    print('usage: user_delete NAME')


def complete_user_delete(self, text, line, beg, end):
    return tab_completer(self.do_user_list('', True), text)


def do_user_delete(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_user_delete()
        return

    name = args[0]

    if self.user_confirm('Delete this user [y/N]:'):
        self.client.user.delete(self.session, name)

####################


def help_user_disable(self):
    print('user_disable: Disable an user account')
    print('usage: user_disable NAME')


def complete_user_disable(self, text, line, beg, end):
    return tab_completer(self.do_user_list('', True), text)


def do_user_disable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_user_disable()
        return

    name = args[0]

    self.client.user.disable(self.session, name)

####################


def help_user_enable(self):
    print('user_enable: Enable an user account')
    print('usage: user_enable NAME')


def complete_user_enable(self, text, line, beg, end):
    return tab_completer(self.do_user_list('', True), text)


def do_user_enable(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 1:
        self.help_user_enable()
        return

    name = args[0]

    self.client.user.enable(self.session, name)

####################


def help_user_list(self):
    print('user_list: List all users')
    print('usage: user_list')


def do_user_list(self, args, doreturn=False):
    users = self.client.user.listUsers(self.session)
    users = [u.get('login') for u in users]

    if doreturn:
        return users
    else:
        if users:
            print('\n'.join(sorted(users)))

####################


def help_user_listavailableroles(self):
    print('user_listavailableroles: List all available roles for users')
    print('usage: user_listavailableroles')


def do_user_listavailableroles(self, args, doreturn=False):
    roles = self.client.user.listAssignableRoles(self.session)

    if doreturn:
        return roles
    else:
        if roles:
            print('\n'.join(sorted(roles)))

####################


def help_user_addrole(self):
    print('user_addrole: Add a role to an user account')
    print('usage: user_addrole USER ROLE')


def complete_user_addrole(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) == 3:
        return tab_completer(self.do_user_listavailableroles('', True),
                             text)


def do_user_addrole(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_addrole()
        return

    user = args[0]
    role = args[1]

    self.client.user.addRole(self.session, user, role)

####################


def help_user_removerole(self):
    print('user_removerole: Remove a role from an user account')
    print('usage: user_removerole USER ROLE')


def complete_user_removerole(self, text, line, beg, end):
    parts = line.split(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) == 3:
        # only list the roles currently assigned to this user
        roles = self.client.user.listRoles(self.session, parts[1])
        return tab_completer(roles, text)


def do_user_removerole(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_removerole()
        return

    user = args[0]
    role = args[1]

    self.client.user.removeRole(self.session, user, role)

####################


def help_user_details(self):
    print('user_details: Show the details of an user')
    print('usage: user_details USER ...')


def complete_user_details(self, text, line, beg, end):
    return tab_completer(self.do_user_list('', True), text)


def do_user_details(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if not args:
        self.help_user_details()
        return

    add_separator = False

    for user in args:
        try:
            details = self.client.user.getDetails(self.session, user)

            roles = self.client.user.listRoles(self.session, user)

            groups = \
                self.client.user.listAssignedSystemGroups(self.session,
                                                          user)

            default_groups = \
                self.client.user.listDefaultSystemGroups(self.session,
                                                         user)
        except xmlrpclib.Fault:
            logging.warning('%s is not a valid user' % user)
            continue

        org_details = self.client.org.getDetails(self.session,
                                                 details.get('org_id'))
        organization = org_details.get('name')

        if add_separator:
            print(self.SEPARATOR)
        add_separator = True

        print('Username:      %s' % user)
        print('First Name:    %s' % details.get('first_name'))
        print('Last Name:     %s' % details.get('last_name'))
        print('Email Address: %s' % details.get('email'))
        print('Organization:  %s' % organization)
        print('Last Login:    %s' % details.get('last_login_date'))
        print('Created:       %s' % details.get('created_date'))
        print('Enabled:       %s' % details.get('enabled'))

        if roles:
            print('')
            print('Roles')
            print('-----')
            print('\n'.join(sorted(roles)))

        if groups:
            print('')
            print('Assigned Groups')
            print('---------------')
            print('\n'.join(sorted([g.get('name') for g in groups])))

        if default_groups:
            print('')
            print('Default Groups')
            print('--------------')
            print('\n'.join(sorted([g.get('name') for g in default_groups])))

####################


def help_user_addgroup(self):
    print('user_addgroup: Add a group to an user account')
    print('usage: user_addgroup USER <GROUP ...>')


def complete_user_addgroup(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_group_list('', True), parts[-1])


def do_user_addgroup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_user_addgroup()
        return

    user = args.pop(0)
    groups = args

    self.client.user.addAssignedSystemGroups(self.session,
                                             user,
                                             groups,
                                             False)

####################


def help_user_adddefaultgroup(self):
    print('user_adddefaultgroup: Add a default group to an user account')
    print('usage: user_adddefaultgroup USER <GROUP ...>')


def complete_user_adddefaultgroup(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return tab_completer(self.do_group_list('', True), parts[-1])


def do_user_adddefaultgroup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_user_adddefaultgroup()
        return

    user = args.pop(0)
    groups = args

    self.client.user.addDefaultSystemGroups(self.session,
                                            user,
                                            groups)

####################


def help_user_removegroup(self):
    print('user_removegroup: Remove a group to an user account')
    print('usage: user_removegroup USER <GROUP ...>')


def complete_user_removegroup(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        # only list the groups currently assigned to this user
        groups = self.client.user.listAssignedSystemGroups(self.session,
                                                           parts[1])
        return tab_completer([g.get('name') for g in groups], parts[-1])


def do_user_removegroup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_user_removegroup()
        return

    user = args.pop(0)
    groups = args

    self.client.user.removeAssignedSystemGroups(self.session,
                                                user,
                                                groups,
                                                True)

####################


def help_user_removedefaultgroup(self):
    print('user_removedefaultgroup: Remove a default group from an ' +
          'user account')
    print('usage: user_removedefaultgroup USER <GROUP ...>')


def complete_user_removedefaultgroup(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append('')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        # only list the groups currently assigned to this user
        groups = self.client.user.listDefaultSystemGroups(self.session,
                                                          parts[1])
        return tab_completer([g.get('name') for g in groups], parts[-1])


def do_user_removedefaultgroup(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) < 2:
        self.help_user_removedefaultgroup()
        return

    user = args.pop(0)
    groups = args

    self.client.user.removeDefaultSystemGroups(self.session,
                                               user,
                                               groups)

####################


def help_user_setfirstname(self):
    print('user_setfirstname: Set an user accounts first name field')
    print('usage: user_setfirstname USER FIRST_NAME')


def complete_user_setfirstname(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return


def do_user_setfirstname(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_setfirstname()
        return

    user = args.pop(0)
    details = {'first_name': args.pop(0)}

    self.client.user.setDetails(self.session, user, details)

####################


def help_user_setlastname(self):
    print('user_setlastname: Set an user accounts last name field')
    print('usage: user_setlastname USER LAST_NAME')


def complete_user_setlastname(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return


def do_user_setlastname(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_setlastname()
        return

    user = args.pop(0)
    details = {'last_name': args.pop(0)}

    self.client.user.setDetails(self.session, user, details)

####################


def help_user_setemail(self):
    print('user_setemail: Set an user accounts email field')
    print('usage: user_setemail USER EMAIL')


def complete_user_setemail(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return


def do_user_setemail(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_setemail()
        return

    user = args.pop(0)
    details = {'email': args.pop(0)}

    self.client.user.setDetails(self.session, user, details)

####################


def help_user_setprefix(self):
    print('user_setprefix: Set an user accounts name prefix field')
    print('usage: user_setprefix USER PREFIX')


def complete_user_setprefix(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return


def do_user_setprefix(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) > 2:
        self.help_user_setprefix()
        return

    user = args.pop(0)
    if not args:
        # clearing prefix with a space currently does not work
        # spacewalk requires a space to clear the prefix but the
        # space seems to be stripped when submitted to the API gateway
        # attempts to use %x20 and \u0020 (among others) also fail
        details = {'prefix': ' '}
    else:
        details = {'prefix': args.pop(0)}

    self.client.user.setDetails(self.session, user, details)

####################


def help_user_setpassword(self):
    print('user_setpassword: Set an user accounts name prefix field')
    print('usage: user_setpassword USER PASSWORD')


def complete_user_setpassword(self, text, line, beg, end):
    parts = shlex.split(line)
    if line[-1] == ' ':
        parts.append(' ')

    if len(parts) == 2:
        return tab_completer(self.do_user_list('', True), text)
    elif len(parts) > 2:
        return


def do_user_setpassword(self, args):
    arg_parser = get_argument_parser()

    (args, _options) = parse_command_arguments(args, arg_parser)

    if len(args) != 2:
        self.help_user_setpassword()
        return

    user = args.pop(0)
    details = {'password': args.pop(0)}

    self.client.user.setDetails(self.session, user, details)
0707010000002C000081B40000000000000000000000015DA8415F000061E2000000000000000000000000000000000000001F00000000spacecmd/src/spacecmd/utils.py#
# Licensed under the GNU General Public License Version 3
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2013 Aron Parsons <aronparsons@gmail.com>
# Copyright (c) 2011--2018 Red Hat, Inc.
#

# NOTE: the 'self' variable is an instance of SpacewalkShell

# wildcard import
# pylint: disable=W0401,W0614

# unused argument
# pylint: disable=W0613

# invalid function name
# pylint: disable=C0103

import logging
import os
import pickle
import re
import readline
import shlex
import sys
import time
import argparse

try:
    from xmlrpc import client as xmlrpclib
except ImportError:
    import xmlrpclib
from collections import deque
from datetime import datetime, timedelta
from difflib import unified_diff
from tempfile import mkstemp
from textwrap import wrap
from subprocess import Popen, PIPE

try:
    import json
except ImportError:
    import simplejson as json  # python < 2.6

import rpm

from spacecmd.argumentparser import SpacecmdArgumentParser

__EDITORS = ['vim', 'vi', 'nano', 'emacs']


class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj,xmlrpclib.DateTime):
            return datetime.fromtimestamp(time.mktime(obj.timetuple())).strftime("%F %T")
        return json.JSONEncoder.default(self, obj)


def get_argument_parser():
    return SpacecmdArgumentParser()

def parse_command_arguments(command_args, argument_parser, glob=True):
    try:
        parts = shlex.split(command_args)

        # allow simple globbing
        if glob:
            parts = [re.sub(r'\*', '.*', a) for a in parts]

        argument_parser.add_argument('leftovers', nargs='*',
                                     help=argparse.SUPPRESS)
        opts = argument_parser.parse_args(args=parts)
        if opts.leftovers:
            leftovers = opts.leftovers
        else:
            leftovers = []
        return leftovers, opts
    except IndexError:
        return None, None


# check if any named options were passed to the function, and if so,
# declare that the function is non-interactive
# note: because we do it this way, default options are not passed into
# OptionParser, as it would make determining if any options were passed
# too complex
def is_interactive(options):
    for key in options.__dict__:
        if options.__dict__[key]:
            return False

    return True


def load_cache(cachefile):
    data = {}
    expire = datetime.now()

    logging.debug('Loading cache from %s', cachefile)

    if os.path.isfile(cachefile):
        try:
            inputfile = open(cachefile, 'rb')
            data = pickle.load(inputfile)
            inputfile.close()
        except EOFError:
            # If cache generation is interrupted (e.g by ctrl-c) you can end up
            # with an EOFError exception due to the partial picked file
            # So we catch this error and remove the corrupt partial file
            # If you don't do this then spacecmd will fail with an unhandled
            # exception until the partial file is manually removed
            logging.warning("Loading cache file %s failed", cachefile)
            logging.warning("Cache generation was probably interrupted," +
                            "removing corrupt %s", cachefile)
            os.remove(cachefile)
        except IOError:
            logging.error("Couldn't load cache from %s", cachefile)

        if isinstance(data, (list, dict)):
            if 'expire' in data:
                expire = data['expire']
                del data['expire']
    else:
        logging.debug('%s does not exist', cachefile)

    return data, expire


def save_cache(cachefile, data, expire=None):
    if expire:
        data['expire'] = expire

    try:
        output = open(cachefile, 'wb')
        pickle.dump(data, output, pickle.HIGHEST_PROTOCOL)
        output.close()
    except IOError:
        logging.error("Couldn't write to %s", cachefile)

    if 'expire' in data:
        del data['expire']


def tab_completer(options, text):
    return [o for o in options if re.match(text, o)]


def filter_results(items, patterns, search=False):
    matches = []

    compiled_patterns = []
    for pattern in patterns:
        if search:
            compiled_patterns.append(re.compile(pattern, re.I))
        else:
            # If in "match" mode, we don't want to match substrings
            compiled_patterns.append(re.compile("^" + pattern + "$", re.I))

    for item in items:
        for pattern in compiled_patterns:
            if search:
                result = pattern.search(item)
            else:
                result = pattern.match(item)

            if result:
                matches.append(item)
                break

    return matches


def editor(template='', delete=False):
    # create a temporary file
    (descriptor, file_name) = mkstemp(prefix='spacecmd.')

    if template and descriptor:
        try:
            handle = os.fdopen(descriptor, 'w')
            handle.write(template)
            handle.close()
        except IOError:
            logging.warning('Could not open the temporary file')

    # use the user's specified editor
    if 'EDITOR' in os.environ:
        if __EDITORS[0] != os.environ['EDITOR']:
            __EDITORS.insert(0, os.environ['EDITOR'])

    success = False
    for editor_cmd in __EDITORS:
        try:
            exit_code = os.spawnlp(os.P_WAIT, editor_cmd,
                                   editor_cmd, file_name)

            if exit_code == 0:
                success = True
                break
            else:
                logging.error('Editor exited with code %i', exit_code)
        except OSError:
            pass

    if not success:
        logging.error('No editors found')
        return ''

    if os.path.isfile(file_name) and exit_code == 0:
        try:
            # read the session (format = username:session)
            handle = open(file_name, 'r')
            contents = handle.read()
            handle.close()

            if delete:
                try:
                    os.remove(file_name)
                    file_name = ''
                except OSError:
                    logging.error('Could not remove %s', file_name)

            return (contents, file_name)
        except IOError:
            logging.error('Could not read %s', file_name)
            return ([], '')


def prompt_user(prompt, noblank=False, multiline=False):
    try:
        while True:
            if multiline:
                print(prompt)
                userinput = sys.stdin.read()
            else:
                try:
                    # python 2 must call raw_input() because input()
                    # also evaluates the user input and that causes
                    # problems.
                    userinput = raw_input('%s ' % prompt)
                except NameError:
                    # python 3 replaced raw_input() with input()...
                    # it no longer evaulates the user input.
                    userinput = input('%s ' % prompt)
            if noblank:
                if userinput != '':
                    break
            else:
                break
    except EOFError:
        print('')
        return ''

    if userinput != '':
        last = readline.get_current_history_length() - 1

        if last >= 0:
            readline.remove_history_item(last)

    return userinput


# parse time input from the user and return xmlrpclib.DateTime
def parse_time_input(userinput=''):
    timestamp = None

    if userinput == '' or re.match('now', userinput, re.I):
        timestamp = datetime.now()

    # handle YYYMMDDHHMM times
    if not timestamp:
        match = re.match(r'^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?$', userinput)

        if match:
            date_format = '%Y%m%d'

            # YYYYMMDD
            if not match.group(4) and not match.group(5):
                timestamp = time.strptime('%s%s%s' % (match.group(1),
                                                      match.group(2),
                                                      match.group(3)),
                                          date_format)
            # YYYYMMDDHH
            elif not match.group(5):
                date_format += '%H'

                timestamp = time.strptime('%s%s%s%s' % (match.group(1),
                                                        match.group(2),
                                                        match.group(3),
                                                        match.group(4)),
                                          date_format)
            # YYYYMMDDHHMM
            elif not match.group(6):
                date_format += '%H%M'

                timestamp = time.strptime('%s%s%s%s%s' % (match.group(1),
                                                          match.group(2),
                                                          match.group(3),
                                                          match.group(4),
                                                          match.group(5)),
                                          date_format)
            # YYYYMMDDHHMMSS
            else:
                date_format += '%H%M%S'

                timestamp = time.strptime('%s%s%s%s%s%s' % (match.group(1),
                                                            match.group(2),
                                                            match.group(3),
                                                            match.group(4),
                                                            match.group(5),
                                                            match.group(6)),
                                          date_format)
            if timestamp:
                # 2.5 has a nice little datetime.strptime() function...
                timestamp = datetime(*(timestamp)[0:7])

    # handle time differences (e.g., +1m, +2h)
    if not timestamp:
        match = re.search(r'^(\+|-)?(\d+)(s|m|h|d)$', userinput, re.I)

        if match and len(match.groups()) >= 2:
            sign = match.group(1)
            number = int(match.group(2))
            unit = match.group(3)

            if sign == '-':
                number = -number

            if re.match('s', unit, re.I):
                delta = timedelta(seconds=number)
            elif re.match('m', unit, re.I):
                delta = timedelta(minutes=number)
            elif re.match('h', unit, re.I):
                delta = timedelta(hours=number)
            elif re.match('d', unit, re.I):
                delta = timedelta(days=number)

            timestamp = datetime.now() + delta

    if timestamp:
        return xmlrpclib.DateTime(timestamp.timetuple())

    logging.error('Invalid time provided')
    return


# Compares 2 package objects (dicts) and returns the newest one.
# If the objects are the same, we return None
def latest_pkg(pkg1, pkg2, version_key='version',
               release_key='release', epoch_key='epoch'):
    # Sometimes empty epoch is a space, sometimes its an empty string, which
    # breaks the comparison, strip it here to fix
    t1 = (pkg1[epoch_key].strip(), pkg1[version_key], pkg1[release_key])
    t2 = (pkg2[epoch_key].strip(), pkg2[version_key], pkg2[release_key])

    result = rpm.labelCompare(t1, t2) # pylint: disable=no-member
    if result == 1:
        return pkg1
    if result == -1:
        return pkg2

    return None

# build a proper RPM name from the various parts


def build_package_names(packages):
    single = False

    if not isinstance(packages, list):
        packages = [packages]
        single = True

    package_names = []
    for p in packages:
        package = '%s-%s-%s' % (
            p.get('name'), p.get('version'), p.get('release'))

        if p.get('epoch') != ' ' and p.get('epoch') != '':
            package += ':%s' % p.get('epoch')

        if p.get('arch'):
            # system.listPackages uses AMD64 instead of x86_64
            arch = re.sub('AMD64', 'x86_64', p.get('arch'))

            package += '.%s' % arch
        elif p.get('arch_label'):
            package += '.%s' % p.get('arch_label')

        package_names.append(package)

    if single:
        return package_names[0]

    package_names.sort()
    return package_names


def print_errata_summary(erratum):
    # Workaround - recent spacewalk lacks the "date" key
    # on some listErrata calls
    if erratum.get('date') is None:
        erratum['date'] = erratum.get('issue_date')
    if erratum['date'] is None:
        erratum['date'] = "no_date"
    date_parts = erratum['date'].split()

    if len(date_parts) > 1:
        erratum['date'] = date_parts[0]

    print('%s  %s  %s' % (
        erratum.get('advisory_name').ljust(14),
        wrap(erratum.get('advisory_synopsis'), 50)[0].ljust(50),
        erratum.get('date').rjust(8)))


def print_errata_list(errata):
    rhsa = []
    rhea = []
    rhba = []

    for erratum in errata:
        if re.match('security', erratum.get('advisory_type'), re.I):
            rhsa.append(erratum)
        elif re.match('bug fix', erratum.get('advisory_type'), re.I):
            rhba.append(erratum)
        elif re.match('product enhancement', erratum.get('advisory_type'), re.I):
            rhea.append(erratum)
        else:
            logging.warning('%s is an unknown errata type',
                            erratum.get('advisory_name'))
            continue

    if not errata:
        return

    if rhsa:
        print('Security Errata')
        print('---------------')
        for erratum in rhsa:
            print_errata_summary(erratum)

    if rhba:
        if rhsa:
            print('')

        print('Bug Fix Errata')
        print('--------------')
        for erratum in rhba:
            print_errata_summary(erratum)

    if rhea:
        if rhsa or rhba:
            print('')

        print('Enhancement Errata')
        print('------------------')
        for erratum in rhea:
            print_errata_summary(erratum)


def config_channel_order(all_channels=None, new_channels=None):
    all_channels = all_channels or []
    new_channels = new_channels or []
    while True:
        print('Current Selections')
        print('------------------')
        for i, new_channel in enumerate(new_channels, 1):
            print('%i. %s' % (i, new_channel))

        print('')
        action = prompt_user('a[dd], r[emove], c[lear], d[one]:')

        if re.match('a', action, re.I):
            print('')
            print('Available Configuration Channels')
            print('--------------------------------')
            for c in sorted(all_channels):
                print(c)

            print('')
            channel = prompt_user('Channel:')

            if channel not in all_channels:
                logging.warning('Invalid channel')
                continue

            try:
                rank = int(prompt_user('New Rank:'))

                if channel in new_channels:
                    new_channels.remove(channel)

                new_channels.insert(rank - 1, channel)
            except IndexError:
                logging.warning('Invalid rank')
                continue
            except ValueError:
                logging.warning('Invalid rank')
                continue
        elif re.match('r', action, re.I):
            channel = prompt_user('Channel:')

            if channel not in all_channels:
                logging.warning('Invalid channel')
                continue

            new_channels.remove(channel)
        elif re.match('c', action, re.I):
            print('Clearing current selections')
            new_channels = []
            continue
        elif re.match('d', action, re.I):
            break

        print('')

    return new_channels


def list_locales():
    if not os.path.isdir('/usr/share/zoneinfo'):
        return []

    zones = []

    for item in os.listdir('/usr/share/zoneinfo'):
        path = os.path.join('/usr/share/zoneinfo', item)

        if os.path.isdir(path):
            try:
                for subitem in os.listdir(path):
                    zones.append(os.path.join(item, subitem))
            except IOError:
                logging.error('Could not read %s', path)
        else:
            zones.append(item)

    return zones


# find the longest string in a list
def max_length(items, minimum=0):
    max_size = 1
    for item in items:
        if len(item) > max_size:
            max_size = len(item)

    if max_size < minimum:
        max_size = minimum

    return max_size


# read in a file
def read_file(filename):
    handle = open(filename, 'r')
    contents = handle.read()
    handle.close()

    return contents


def parse_str(s, type_to=None):
    """
    Similar to 'read :: Read a => String -> a' in Haskell.

    >>> parse_str('1234567', int)
    1234567
    >>> parse_str('1234567')
    1234567
    >>> parse_str('abcXYZ012')
    'abcXYZ012'
    >>> d = dict(channelLabel="foo-i386-5")
    >>> d = parse_str('{"channelLabel": "foo-i386-5"}')
    >>> assert d["channelLabel"] == 'foo-i386-5'

    """
    try:
        if type_to is not None and isinstance(type_to, type):
            return type_to(s)

        if re.match(r'[1-9]\d*', s):
            return int(s)

        if re.match(r'{.*}', s):
            return json.loads(s)  # retry with json module

        return str(s)

    except ValueError:
        return str(s)


def parse_list_str(list_s, sep=","):
    """
    simple parser for a list of items separated with "," (comma) or given
    separator chars.

    >>> assert parse_list_str("") == []
    >>> assert parse_list_str("a,b") == ["a", "b"]
    >>> assert parse_list_str("a,b,") == ["a", "b"]
    >>> assert parse_list_str("a:b:", ":") == ["a", "b"]
    """
    return [p for p in list_s.split(sep) if p]


def parse_api_args(args, sep=','):
    """
    Simple JSON-like expression parser.

    :param args: a list of strings may be separated with sep, and each
                 string represents parameters passed to API later.
    :type args:  `str`

    :param sep: A char to separate paramters in `args`
    :type sep:  `str`

    :rtype:  rpc arg objects, [arg] :: [string]

    >>> parse_api_args('')
    []
    >>> parse_api_args('1234567')
    [1234567]
    >>> parse_api_args('abcXYZ012')
    ['abcXYZ012']

    >>> assert parse_api_args('{"channelLabel": "foo-i386-5"}')[0]["channelLabel"] == "foo-i386-5"

    >>> (i, s, d) = parse_api_args('1234567,abcXYZ012,{"channelLabel": "foo-i386-5"}')
    >>> assert i == 1234567
    >>> assert s == "abcXYZ012"
    >>> assert d["channelLabel"] == "foo-i386-5"

    >>> (i, s, d) = parse_api_args('[1234567,"abcXYZ012",{"channelLabel": "foo-i386-5"}]')
    >>> assert i == 1234567
    >>> assert s == "abcXYZ012"
    >>> assert d["channelLabel"] == "foo-i386-5"
    """
    if not args:
        return []

    try:
        x = json.loads(args)
        ret = x if isinstance(x, list) else [x]

    except ValueError:
        ret = [parse_str(a) for a in parse_list_str(args, sep)]

    return ret


def json_dump(obj, fp, indent=4, **kwargs):
    json.dump(obj, fp, ensure_ascii=False, indent=indent, **kwargs)


def json_dump_to_file(obj, filename):
    json_data = json.dumps(obj, indent=4, sort_keys=True)

    if json_data is None:
        logging.error("Could not generate json data object!")
        return False

    try:
        fd = open(filename, 'w')
        fd.write(json_data)
        fd.close()
    except IOError as E:
        logging.error("Could not open file %s for writing, permissions?",
                      filename)
        print(E.strerror)
        return False

    return True


def json_read_from_file(filename):
    try:
        data = open(filename).read()
        try:
            jsondata = json.loads(data)
            return jsondata
        except ValueError:
            print("could not read in data from %s" % filename)
    except IOError:
        print("could not open file %s for reading, check permissions?" % filename)
        return None


def get_string_diff_dicts(string1, string2, sep="-"):
    """
    compares two strings and determine, if one string can be transformed into the other by simple string replacements.

    If these strings are closly related, it returns two dictonaries of regular expressions.

    The first dictionary can be used to transfrom type 1 strings into type 2 strings.
    The second dictionary vice versa.

    These replacements blocks must be separated by "-".

    Example:
    string1: rhel6-x86_64-dev-application1
    string2: rhel6-x86_64-qas-application1

    Result:
    dict1: {'(^|-)dev(-|$)': '\\1DIFF(dev|qas)\\2'}
    dict2: {'(^|-)qas(-|$)': '\\1DIFF(dev|qas)\\2'}
    """
    replace1 = {}
    replace2 = {}

    if string1 == string2:
        logging.info("Skipping usage of common strings: both strings are equal")
        return [None, None]
    substrings1 = deque(string1.split(sep))
    substrings2 = deque(string2.split(sep))

    while substrings1 and substrings2:
        sub1 = substrings1.popleft()
        sub2 = substrings2.popleft()
        if sub1 == sub2:
            # equal, nothing to do
            pass
        else:
            # TODO: replace only if len(sub1) == len(sub2) ?
            replace1['(^|-)' + sub1 + '(-|$)'] = r'\1' + "DIFF(" + sub1 + "|" + sub2 + ")" + r'\2'
            replace2['(^|-)' + sub2 + '(-|$)'] = r'\1' + "DIFF(" + sub1 + "|" + sub2 + ")" + r'\2'
    if substrings1 or substrings2:
        logging.info("Skipping usage of common strings: number of substrings differ")
        return [None, None]
    return [replace1, replace2]


def replace(line, replacedict):
    if replacedict:
        for source in replacedict:
            line = re.sub(source, replacedict[source], line)
    return line


def get_normalized_text(text, replacedict=None, excludes=None):
    # parts of the data inside the spacewalk component information
    # are not really differences between two instances.
    # Therefore parts of the data will be modified before the diff:
    # - specific lines, starting with a defined keyword, will be excluded
    # - specific character sequences will be replaced with the same text in both instances.
    # Example:
    # we want to compare two related activationkeys:
    # "1-rhel6-x86_64-dev" and "1-rhel6-x86_64-prd"
    # ("dev" for "development" and "prd" for "production").
    # We assume that the "dev" activationkey "1-rhel6-x86_64-dev"
    # has references to other "dev" components,
    # while the "prd" activationkey "1-rhel6-x86_64-prd"
    # has references to other "prd" components.
    # Therefore we replace all occurrences of "dev" in "1-rhel6-x86_64-dev"
    # and all occurrences of "prd" in "1-rhel6-x86_64-prd"
    # with the common string "DIFF(dev|prd)".
    # What differences are to be replaced is guessed
    # from the name differences of there components.
    # This will not work always, but it help in a lot of cases.

    normalized_text = []
    if text:
        for st in text:
            for line in st.split("\n"):
                if not excludes:
                    normalized_text.append(replace(line, replacedict))
                # We do it this way instead of passing a tuple to
                # line.startswith to allow compatibility with python 2.4
                elif not [e for e in excludes if line.startswith(e)]:
                    normalized_text.append(replace(line, replacedict))
                else:
                    logging.debug("excluding line: " + line)
    return normalized_text


def diff(source_data, target_data, source_channel, target_channel):
    return list(unified_diff(source_data, target_data, source_channel, target_channel))


def file_is_binary(self, path):
    """Tries to determine whether the file is a binary.
       Assumes binary if it can't categorize the file as plaintext"""
    try:
        process = Popen(["file", "-b", "--mime-type", path], stdout=PIPE)
        output = process.communicate()[0]
        exit_code = process.wait()
        if exit_code != 0:
            return True

        if output.startswith("text/"):
            return False
    except OSError:
        pass
    return True


def string_to_bool(input_string):
    if not isinstance(input_string, bool):
        return input_string.lower().rstrip(' ') == 'true'
    return input_string
0707010000002D000041FD0000000000000000000000015DA8415F00000000000000000000000000000000000000000000000F00000000spacecmd/tests0707010000002E000081B40000000000000000000000015DA8415F0000070C000000000000000000000000000000000000001A00000000spacecmd/tests/helpers.py# coding: utf-8
"""
Testing helpers
"""
import time
import pytest
import hashlib
from mock import MagicMock
from io import StringIO


class FileHandleMock(StringIO):
    """
    Filehandle mock
    """
    def __init__(self):
        self._init_params = None
        StringIO.__init__(self)
        self._closed = False

    def __call__(self, *args, **kwargs):
        self.__init_params = args, kwargs
        return self

    def close(self):
        """
        Bypass closing.
        """
        self._closed = True

    def get_content(self):
        """
        Get file content.
        """
        self.seek(0)
        return self.read()

    def get_init_args(self):
        """
        Get initial arguments.
        """
        return self.__init_params[0]

    def get_init_kwargs(self):
        """
        Get initial keywords.
        """
        return self.__init_params[1]


@pytest.fixture
def shell():
    """
    Create fake shell.
    """
    base = MagicMock()
    base.session = hashlib.sha256(str(time.time()).encode("utf-8")).hexdigest()
    base.client = MagicMock()
    base.client.activationkey = MagicMock()
    base.do_activationkey_list = MagicMock(return_value="do_activation_list")

    return base

def assert_expect(calls, *expectations):
    """
    Check expectations.
    Function accepts a list of calls of some mock and the corresponding expectations.
    Result is counted as passed, when calls are identical to the expectations and
    no more, no less. Otherwise an assertion error is raised.

    :param calls: Mock's call_args_list
    :param expectations: expectations array.
    """
    expectations = list(expectations)
    for call in calls:
        assert call[0][0] == next(iter(expectations))
        expectations.pop(0)
    assert not expectations
0707010000002F000081B40000000000000000000000015DA8415F00015B09000000000000000000000000000000000000002500000000spacecmd/tests/test_activationkey.py# coding: utf-8
"""
Test activation key methods.
"""
from mock import MagicMock, patch
import pytest
import time
import hashlib
import spacecmd.activationkey
from xmlrpc import client as xmlrpclib
from helpers import shell


class TestSCActivationKey:
    """
    Test activation key.
    """
    def test_completer_ak_addpackages(self, shell):
        """
        Test tab completer activation keys on addpackages.
        """
        text = "Communications satellite used by the military for star wars."
        completer = MagicMock()
        with patch("spacecmd.activationkey.tab_completer", completer):
            spacecmd.activationkey.complete_activationkey_addpackages(shell, text, "do this", None, None)
            assert completer.called
            call_id, ret_text = completer.call_args_list[0][0]
            assert call_id == "do_activation_list"
            assert ret_text == text


class TestSCActivationKeyMethods:
    """
    Test actuvation key methods.
    """
    def test_do_activationkey_addpackages_noargs(self, shell):
        """
        Test add packages method call shows help on no args.
        """
        shell.help_activationkey_addpackages = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        spacecmd.activationkey.do_activationkey_addpackages(shell, "")
        assert shell.help_activationkey_addpackages.called

    def test_do_activationkey_addpackages_help_args(self, shell):
        """
        Test add packages method call shows help on help args passed.
        """
        shell.help_activationkey_addpackages = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        spacecmd.activationkey.do_activationkey_addpackages(shell, "help")
        assert shell.help_activationkey_addpackages.called

    def test_do_activationkey_addpackages_args(self, shell):
        """
        Test add packages method call shows help on args passed.
        """
        shell.help_activationkey_addpackages = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        spacecmd.activationkey.do_activationkey_addpackages(shell, "call something here")
        assert not shell.help_activationkey_addpackages.called
        assert shell.client.activationkey.addPackages.called
        session, fun, args = shell.client.activationkey.addPackages.call_args_list[0][0]
        assert session == shell.session
        assert fun == "call"
        assert isinstance(args, list)
        assert len(args) == 2
        for arg in args:
            assert arg["name"] in ["something", "here"]

    def test_do_activationkey_removepackages_noargs(self, shell):
        """
        Test remove packages method call shows help on no args.
        """
        shell.help_activationkey_removepackages = MagicMock()
        shell.client.activationkey.removePackages = MagicMock()

        # TODO: Add help for remove packages!
        spacecmd.activationkey.do_activationkey_removepackages(shell, "")
        assert not shell.help_activationkey_removePackages.called

    def test_do_activationkey_removepackages_help_args(self, shell):
        """
        Test remove packages method call shows help if only one argument is passed.
        """
        shell.help_activationkey_removepackages = MagicMock()
        shell.client.activationkey.removePackages = MagicMock()

        spacecmd.activationkey.do_activationkey_removepackages(shell, "key")
        assert shell.help_activationkey_removepackages.called

    def test_do_activationkey_removepackages_args(self, shell):
        """
        Test remove packages method calls "removePackages" API call.
        """
        shell.help_activationkey_removepackages = MagicMock()
        shell.client.activationkey.removePackages = MagicMock()

        spacecmd.activationkey.do_activationkey_removepackages(shell, "key package")
        assert not shell.help_activationkey_removepackages.called
        assert shell.client.activationkey.removePackages.called
        session, fun, args = shell.client.activationkey.removePackages.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert "name" in args[0]
        assert args[0]["name"] == "package"

    def test_do_activationkey_addgroups_noargs(self, shell):
        """
        Test addgroup without args calls help.
        """
        shell.help_activationkey_addgroups = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()

        spacecmd.activationkey.do_activationkey_addgroups(shell, "")
        assert shell.help_activationkey_addgroups.called

    def test_do_activationkey_addgroups_help_args(self, shell):
        """
        Test add groups method call shows help if only one argument is passed.
        """
        shell.help_activationkey_addgroups = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()

        spacecmd.activationkey.do_activationkey_addgroups(shell, "key")
        assert shell.help_activationkey_addgroups.called

    def test_do_activationkey_addgroups_args(self, shell):
        """
        Test "addgroups" method calls "addServerGroups" API call.
        """
        shell.help_activationkey_addgroups = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value={"id": 42})

        spacecmd.activationkey.do_activationkey_addgroups(shell, "key group")
        assert not shell.help_activationkey_addgroups.called
        assert shell.client.activationkey.addServerGroups.called
        session, fun, args = shell.client.activationkey.addServerGroups.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == [42]

    def test_do_activationkey_removegroups_noargs(self, shell):
        """
        Test removegroup without args calls help.
        """
        shell.help_activationkey_removegroups = MagicMock()
        shell.client.activationkey.removeServerGroups = MagicMock()

        spacecmd.activationkey.do_activationkey_removegroups(shell, "")
        assert shell.help_activationkey_removegroups.called

    def test_do_activationkey_removegroups_help_args(self, shell):
        """
        Test remove groups method call shows help if only one argument is passed.
        """
        shell.help_activationkey_removegroups = MagicMock()
        shell.client.activationkey.removeServerGroups = MagicMock()

        spacecmd.activationkey.do_activationkey_removegroups(shell, "key")
        assert shell.help_activationkey_removegroups.called
        assert not shell.client.activationkey.removeServerGroups.called

    def test_do_activationkey_removegroups_args(self, shell):
        """
        Test "removegroups" method calls "removeServerGroups" API call.
        """
        shell.help_activationkey_removegroups = MagicMock()
        shell.client.activationkey.removeServerGroups = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value={"id": 42})

        spacecmd.activationkey.do_activationkey_removegroups(shell, "key group")
        assert not shell.help_activationkey_removegroups.called
        assert shell.client.activationkey.removeServerGroups.called
        session, fun, args = shell.client.activationkey.removeServerGroups.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == [42]

    def test_do_activationkey_addentitlements_noargs(self, shell):
        """
        Test addentitlements without args calls help.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "")
        assert shell.help_activationkey_addentitlements.called
        assert not shell.client.activationkey.addEntitlements.called

    def test_do_activationkey_addentitlements_help_args(self, shell):
        """
        Test addentitlements method call shows help if only one argument is passed.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "key")
        assert shell.help_activationkey_addentitlements.called
        assert not shell.client.activationkey.addEntitlements.called

    def test_do_activationkey_addentitlements_args(self, shell):
        """
        Test "addentitlements" method calls "addEntitlements" API call.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "key entitlement")
        assert not shell.help_activationkey_addentitlements.called
        assert shell.client.activationkey.addEntitlements.called
        session, fun, args = shell.client.activationkey.addEntitlements.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == ['entitlement']

    def test_do_activationkey_addentitlements_noargs(self, shell):
        """
        Test addentitlements without args calls help.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "")
        assert shell.help_activationkey_addentitlements.called
        assert not shell.client.activationkey.addEntitlements.called

    def test_do_activationkey_addentitlements_help_args(self, shell):
        """
        Test addentitlements method call shows help if only one argument is passed.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "key")
        assert shell.help_activationkey_addentitlements.called
        assert not shell.client.activationkey.addEntitlements.called

    def test_do_activationkey_addentitlements_args(self, shell):
        """
        Test "addentitlements" method calls "addEntitlements" API call.
        """
        shell.help_activationkey_addentitlements = MagicMock()
        shell.client.activationkey.addEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_addentitlements(shell, "key entitlement")
        assert not shell.help_activationkey_addentitlements.called
        assert shell.client.activationkey.addEntitlements.called
        session, fun, args = shell.client.activationkey.addEntitlements.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == ['entitlement']

    def test_do_activationkey_removeentitlements_noargs(self, shell):
        """
        Test removeentitlements without args calls help.
        """
        shell.help_activationkey_removeentitlements = MagicMock()
        shell.client.activationkey.removeEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_removeentitlements(shell, "")
        assert shell.help_activationkey_removeentitlements.called
        assert not shell.client.activationkey.removeEntitlements.called

    def test_do_activationkey_removeentitlements_help_args(self, shell):
        """
        Test removeentitlements method call shows help if only one argument is passed.
        """
        shell.help_activationkey_removeentitlements = MagicMock()
        shell.client.activationkey.removeEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_removeentitlements(shell, "key")
        assert shell.help_activationkey_removeentitlements.called
        assert not shell.client.activationkey.removeEntitlements.called

    def test_do_activationkey_removeentitlements_args(self, shell):
        """
        Test "removeentitlements" method calls "removeEntitlements" API call.
        """
        shell.help_activationkey_removeentitlements = MagicMock()
        shell.client.activationkey.removeEntitlements = MagicMock()

        spacecmd.activationkey.do_activationkey_removeentitlements(shell, "key entitlement")
        assert not shell.help_activationkey_removeentitlements.called
        assert shell.client.activationkey.removeEntitlements.called
        session, fun, args = shell.client.activationkey.removeEntitlements.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == ['entitlement']

    def test_do_activationkey_addchildchannels_noargs(self, shell):
        """
        Test addchildchannels without args calls help.
        """
        shell.help_activationkey_addchildchannels = MagicMock()
        shell.client.activationkey.addChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_addchildchannels(shell, "")
        assert shell.help_activationkey_addchildchannels.called
        assert not shell.client.activationkey.addChildChannels.called

    def test_do_activationkey_addchildchannels_help_args(self, shell):
        """
        Test addchildchannels method call shows help if only one argument is passed.
        """
        shell.help_activationkey_addchildchannels = MagicMock()
        shell.client.activationkey.addChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_addchildchannels(shell, "key")
        assert shell.help_activationkey_addchildchannels.called
        assert not shell.client.activationkey.addChildChannels.called

    def test_do_activationkey_addchildchannels_args(self, shell):
        """
        Test "addchildchannels" method calls "addChildChannels" API call.
        """
        shell.help_activationkey_addchildchannels = MagicMock()
        shell.client.activationkey.addChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_addchildchannels(shell, "key some_channel")
        assert not shell.help_activationkey_addchildchannels.called
        assert shell.client.activationkey.addChildChannels.called
        session, fun, args = shell.client.activationkey.addChildChannels.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == ['some_channel']

    def test_do_activationkey_removechildchannels_noargs(self, shell):
        """
        Test removechildchannels without args calls help.
        """
        shell.help_activationkey_removechildchannels = MagicMock()
        shell.client.activationkey.removeChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_removechildchannels(shell, "")
        assert shell.help_activationkey_removechildchannels.called
        assert not shell.client.activationkey.removeChildChannels.called

    def test_do_activationkey_removechildchannels_help_args(self, shell):
        """
        Test removechildchannels method call shows help if only one argument is passed.
        """
        shell.help_activationkey_removechildchannels = MagicMock()
        shell.client.activationkey.removeChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_removechildchannels(shell, "key")
        assert shell.help_activationkey_removechildchannels.called
        assert not shell.client.activationkey.removeChildChannels.called

    def test_do_activationkey_removechildchannels_args(self, shell):
        """
        Test "removechildchannels" method calls "removeChildChannels" API call.
        """
        shell.help_activationkey_removechildchannels = MagicMock()
        shell.client.activationkey.removeChildChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_removechildchannels(shell, "key some_channel")
        assert not shell.help_activationkey_removechildchannels.called
        assert shell.client.activationkey.removeChildChannels.called
        session, fun, args = shell.client.activationkey.removeChildChannels.call_args_list[0][0]
        assert session == shell.session
        assert fun == "key"
        assert isinstance(args, list)
        assert len(args) == 1
        assert args == ['some_channel']

    def test_do_activationkey_listchildchannels_noargs(self, shell):
        """
        Test listchildchannels command triggers help on no args
        """
        shell.help_activationkey_listchildchannels = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={"child_channel_labels"})

        spacecmd.activationkey.do_activationkey_listchildchannels(shell, "")
        assert shell.help_activationkey_listchildchannels.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_listchildchannels_args(self, shell):
        """
        Test listchildchannels command prints child channels by the activation key passed.
        """
        shell.help_activationkey_listchildchannels = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={
            "child_channel_labels": ["one", "two", "three"]
        })

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listchildchannels(shell, "key")
        assert mprint.call_args_list[0][0][0] == "one\nthree\ntwo"  # Sorted

    def test_do_activationkey_listbasechannel_noargs(self, shell):
        """
        Test listbasechannels command triggers help on no args
        """
        shell.help_activationkey_listbasechannel = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={"base_channel_label"})

        spacecmd.activationkey.do_activationkey_listbasechannel(shell, "")
        assert shell.help_activationkey_listbasechannel.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_listbasechannel_args(self, shell):
        """
        Test listbasechannels command prints base channel by the activation key passed.
        """
        shell.help_activationkey_listbasechannel = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={
            "base_channel_label": "Darth Vader",
        })

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listbasechannel(shell, "key")
        assert mprint.call_args_list[0][0][0] == "Darth Vader"

    def test_do_activationkey_listgroups_noargs(self, shell):
        """
        Test listgroups command triggers help on no args
        """
        shell.help_activationkey_listgroups = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(site_effect=[{"name": "RD-2D"}, {"name": "C-3PO"}])

        spacecmd.activationkey.do_activationkey_listgroups(shell, "")
        assert shell.help_activationkey_listgroups.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_listgroups_args(self, shell):
        """
        Test listgroups command prints groups by the activation key passed.
        """
        shell.help_activationkey_listgroups = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={"server_group_ids": [2, 3]})
        shell.client.systemgroup.getDetails = MagicMock(side_effect=[{"name": "RD-2D"}, {"name": "C-3PO"}])

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listgroups(shell, "key")
        assert len(mprint.call_args_list) == 2
        assert mprint.call_args_list[0][0][0] == "RD-2D"
        assert mprint.call_args_list[1][0][0] == "C-3PO"

    def test_do_activationkey_listentitlements_noargs(self, shell):
        """
        Test listentitlements command triggers help on no args
        """
        shell.help_activationkey_listentitlements = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_listentitlements(shell, "")
        assert shell.help_activationkey_listentitlements.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_listentitlements_args(self, shell):
        """
        Test listentitlements command prints entitlements by the activation key passed.
        """
        shell.help_activationkey_listentitlements = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value={"entitlements": ["one", "two", "three"]})

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listentitlements(shell, "key")
        assert mprint.call_args_list[0][0][0] == 'one\ntwo\nthree'

    def test_do_activationkey_listpackages_noargs(self, shell):
        """
        Test listpackages command triggers help on no args
        """
        shell.help_activationkey_listpackages = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_listpackages(shell, "")
        assert shell.help_activationkey_listpackages.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_listpackages_args_arch(self, shell):
        """
        Test listpackages command prints packages by the activation key passed with the arch included.
        """
        shell.help_activationkey_listpackages = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(
            return_value={"packages": [
                {"name": "libzypp", "arch": "ZX80"},
                {"name": "java-11-openjdk-devel", "arch": "CBM64"},
            ]}
        )

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listpackages(shell, "key")
        assert mprint.called
        assert len(mprint.call_args_list) == 2
        # keep ordering
        assert mprint.call_args_list[0][0][0] == "libzypp.ZX80"
        assert mprint.call_args_list[1][0][0] == "java-11-openjdk-devel.CBM64"

    def test_do_activationkey_listpackages_args_noarch(self, shell):
        """
        Test listpackages command prints packages by the activation key passed without architecture included.
        """
        shell.help_activationkey_listpackages = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(
            return_value={"packages": [
                {"name": "libzypp"},
                {"name": "java-11-openjdk-devel"},
            ]}
        )

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listpackages(shell, "key")
        assert mprint.called
        assert len(mprint.call_args_list) == 2
        # keep ordering
        assert mprint.call_args_list[0][0][0] == "libzypp"
        assert mprint.call_args_list[1][0][0] == "java-11-openjdk-devel"

    def test_do_activationkey_listconfigchannels_noargs(self, shell):
        """
        Test listconfigchannels command triggers help on no args
        """
        shell.help_activationkey_listconfigchannels = MagicMock()
        shell.client.activationkey.listConfigChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_listconfigchannels(shell, "")
        assert shell.help_activationkey_listconfigchannels.called
        assert not shell.client.activationkey.listConfigChannels.called

    def test_do_activationkey_listconfigchannels_args(self, shell):
        """
        Test listconfigchannels command prints entitlements by the activation key passed.
        """
        channels = [
            {"label": "commodore64"},
            {"label": "pascal_for_msdos"},
            {"label": "lightsaber_patches"}
        ]
        shell.help_activationkey_listconfigchannels = MagicMock()
        shell.client.activationkey.listConfigChannels = MagicMock(return_value=channels)

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_listconfigchannels(shell, "key")
        assert mprint.called
        assert mprint.call_args_list[0][0][0] == 'commodore64\nlightsaber_patches\npascal_for_msdos'

    def test_do_activationkey_addconfigchannels_noargs(self, shell):
        """
        Test addconfigchannels command triggers help on no args.
        """
        shell.help_activationkey_addconfigchannels = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_addconfigchannels(shell, "")
        assert shell.help_activationkey_addconfigchannels.called
        assert not shell.client.activationkey.addConfigChannels.called

    def test_do_activationkey_addconfigchannels_unknown_noargs(self, shell):
        """
        Test addconfigchannels command raises an Exception on unknown passed args.
        """
        shell.help_activationkey_addconfigchannels = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()

        with pytest.raises(Exception) as exc:
            spacecmd.activationkey.do_activationkey_addconfigchannels(shell, "--you-shall-not-pass=True")

        assert "unrecognized arguments" in str(exc)
        assert not shell.help_activationkey_addconfigchannels.called
        assert not shell.client.activationkey.addConfigChannels.called

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    def test_do_activationkey_addconfigchannels_check_args_noninteractive(self, shell):
        """
        Test addconfigchannels command calls addConfigChannels API function on params added.
        """
        shell.help_activationkey_addconfigchannels = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_addconfigchannels(shell, "key rd2d-upgrade -b")

        assert not shell.help_activationkey_addconfigchannels.called
        assert shell.client.activationkey.addConfigChannels.called

        session, keys, channels, order = shell.client.activationkey.addConfigChannels.call_args_list[0][0]
        assert shell.session == session
        assert len(keys) == len(channels) == 1
        assert "key" in keys
        assert "rd2d-upgrade" in channels
        assert bool == type(order)
        assert not order

        shell.client.activationkey.addConfigChannels = MagicMock()
        spacecmd.activationkey.do_activationkey_addconfigchannels(shell, "key rd2d-upgrade")
        session, keys, channels, order = shell.client.activationkey.addConfigChannels.call_args_list[0][0]
        assert shell.session == session
        assert len(keys) == len(channels) == 1
        assert "key" in keys
        assert "rd2d-upgrade" in channels
        assert bool == type(order)
        assert order

        shell.client.activationkey.addConfigChannels = MagicMock()
        spacecmd.activationkey.do_activationkey_addconfigchannels(shell, "key rd2d-upgrade -t")
        session, keys, channels, order = shell.client.activationkey.addConfigChannels.call_args_list[0][0]
        assert shell.session == session
        assert len(keys) == len(channels) == 1
        assert "key" in keys
        assert "rd2d-upgrade" in channels
        assert bool == type(order)
        assert order

    def test_do_activationkey_removeconfigchannels_noargs(self, shell):
        """
        Test removeconfigchannels command triggers help on no args.
        """
        shell.help_activationkey_removeconfigchannels = MagicMock()
        shell.client.activationkey.removeConfigChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_removeconfigchannels(shell, "")
        assert shell.help_activationkey_removeconfigchannels.called
        assert not shell.client.activationkey.removeConfigChannels.called
    
    def test_do_activationkey_removeconfigchannels_insuff_args(self, shell):
        """
        Test removeconfigchannels command triggers help on insufficient args.
        """
        shell.help_activationkey_removeconfigchannels = MagicMock()
        shell.client.activationkey.removeConfigChannels = MagicMock()

        spacecmd.activationkey.do_activationkey_removeconfigchannels(shell, "key")
        assert shell.help_activationkey_removeconfigchannels.called
        assert not shell.client.activationkey.removeConfigChannels.called

    def test_do_activationkey_removeconfigchannels_args(self, shell):
        """
        Test removeconfigchannels command is calling removeConfigChannels API by the activation key passed.
        """
        shell.help_activationkey_removeconfigchannels = MagicMock()
        shell.client.activationkey.removeConfigChannels = MagicMock()

        mprint = MagicMock()
        spacecmd.activationkey.do_activationkey_removeconfigchannels(shell, "key some_patches")
        assert not shell.help_activationkey_removeconfigchannels.called
        assert shell.client.activationkey.removeConfigChannels.called

        session, keys, channels = shell.client.activationkey.removeConfigChannels.call_args_list[0][0]
        assert shell.session == session
        assert "key" in keys
        assert "some_patches" in channels
        assert len(keys) == len(channels) == 1

    @patch("spacecmd.activationkey.config_channel_order",
           MagicMock(return_value=["lightsaber_patches", "rd2d_upgrade"]))
    def test_do_activationkey_setconfigchannelorder_noargs(self, shell):
        """
        Test setconfigchannelorder command triggers help on no args.
        """
        for cmd in [""]:
            shell.help_activationkey_setconfigchannelorder = MagicMock()
            shell.client.activationkey.listConfigChannels = MagicMock()
            shell.client.activationkey.setConfigChannels = MagicMock()
            shell.do_configchannel_list = MagicMock()

            spacecmd.activationkey.do_activationkey_setconfigchannelorder(shell, cmd)
            assert shell.help_activationkey_setconfigchannelorder.called
            assert not shell.client.activationkey.setConfigChannels.called

    @patch("spacecmd.activationkey.config_channel_order",
           MagicMock(return_value=["lightsaber_patches", "rd2d_upgrade"]))
    def test_do_activationkey_setconfigchannelorder_args(self, shell):
        """
        Test setconfigchannelorder command triggers setConfigChannels API call with proper function
        """
        shell.help_activationkey_setconfigchannelorder = MagicMock()
        shell.client.activationkey.listConfigChannels = MagicMock()
        shell.client.activationkey.setConfigChannels = MagicMock()
        shell.do_configchannel_list = MagicMock()

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint):
            spacecmd.activationkey.do_activationkey_setconfigchannelorder(shell, "key")
        assert not shell.help_activationkey_setconfigchannelorder.called
        assert shell.client.activationkey.setConfigChannels.called
        assert len(mprint.call_args_list) == 4
        assert mprint.call_args_list[2][0][0] == "[1] lightsaber_patches"
        assert mprint.call_args_list[3][0][0] == "[2] rd2d_upgrade"

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    def test_do_activationkey_create_nointeract_argstest(self, shell):
        """
        Test call activation key API "create".
        """
        shell.client.activationkey.create = MagicMock(return_value="superglue")
        shell.list_base_channels = MagicMock(return_value=["lightsaber_patches_sle42sp8"])

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_create(shell, "")

        assert logger.info.called
        assert shell.client.activationkey.create.called
        assert logger.info.call_args_list[0][0][0] == "Created activation key superglue"

        session, name, descr, bch, entl, universal = shell.client.activationkey.create.call_args_list[0][0]
        assert shell.session == session
        assert name == descr == bch == ""
        assert entl == []
        assert not universal

        shell.client.activationkey.create = MagicMock(return_value="woodblock")
        shell.list_base_channels = MagicMock(return_value=["lightsaber_patches_sle42sp8"])

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_create(
                shell, ("--name lightsaber --description 'The signature weapon of the Jedi Order' "
                        "--base-channel lightsaber_patches_sle42sp8 --entitlements expanded,universe "
                        "--universal"))

        assert logger.info.called
        assert shell.client.activationkey.create.called
        assert logger.info.call_args_list[0][0][0] == "Created activation key woodblock"

        session, name, descr, bch, entl, universal = shell.client.activationkey.create.call_args_list[0][0]
        assert shell.session == session
        assert name == "lightsaber"
        assert "Jedi Order" in descr
        assert bch == "lightsaber_patches_sle42sp8"
        assert entl == ["expanded", "universe"]
        assert universal

    def test_do_activationkey_activationkey_delete_insuff_args(self, shell):
        """
        Test activationkey_delete command triggers help on insufficient args.
        """
        shell.help_activationkey_delete = MagicMock()
        shell.client.activationkey.delete = MagicMock()

        spacecmd.activationkey.do_activationkey_delete(shell, "")
        assert shell.help_activationkey_delete.called
        assert not shell.client.activationkey.delete.called

    @patch("spacecmd.activationkey.filter_results", MagicMock(return_value=["some_patches", "some_stuff"]))
    def test_do_activationkey_activationkey_delete_args(self, shell):
        """
        Test activationkey_delete command is calling "delete" (key) API.
        """
        shell.help_activationkey_delete = MagicMock()
        shell.client.activationkey.delete = MagicMock()

        mprint = MagicMock()
        logger = MagicMock()
        with patch("spacecmd.activationkey.print", mprint) as mpr, \
             patch("spacecmd.activationkey.logging", logger) as lgr:
            spacecmd.activationkey.do_activationkey_delete(shell, "key some*")
            assert not shell.help_activationkey_delete.called

        assert not logger.error.called
        assert logger.debug.called
        assert logger.debug.call_args_list[0][0][0] == ("activationkey_delete called with args"
                                                        " ['key', 'some.*'], keys=['some_patches', 'some_stuff']")
        assert logger.debug.call_args_list[1][0][0] == "Deleting key some_patches"
        assert logger.debug.call_args_list[2][0][0] == "Deleting key some_stuff"

        assert shell.client.activationkey.delete.called
        keys = ["some_stuff", "some_patches"]
        for call in shell.client.activationkey.delete.call_args_list:
            session, keyname = call[0]
            assert shell.session == session
            assert keyname in keys
            keys.pop(keys.index(keyname))
        assert not keys

    @patch("spacecmd.activationkey.filter_results", MagicMock(return_value=["some_patches", "some_stuff"]))
    def test_do_activationkey_activationkey_list_args(self, shell):
        """
        Test activationkey_list command is calling listActivationKeys API.
        """
        shell.client.activationkey.listActivationKeys = MagicMock(
            return_value=[
                {"key": "some_patches", "description": "Some key"},
                {"key": "some_stuff", "description": "Some other key"},
                {"key": "some_reactivation", "description": "Kickstart re-activation key"},
            ]
        )

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint) as mpr:
            ret = sorted(spacecmd.activationkey.do_activationkey_list(shell, "key some*", doreturn=True))
        assert len(ret) == 2
        assert ret == ['some_patches', 'some_stuff']

    def test_do_activationkey_listsystems_noargs(self, shell):
        """
        Test activationkey_listsystems command is invoking help message on insufficient arguments.
        """
        shell.help_activationkey_listsystems = MagicMock()
        shell.client.activationkey.listActivatedSystems = MagicMock()

        spacecmd.activationkey.do_activationkey_listsystems(shell, "")
        assert shell.help_activationkey_listsystems.called
        assert not shell.client.activationkey.listActivatedSystems.called

    def test_do_activationkey_listsystems_args(self, shell):
        """
        Test activationkey_listsystems command is calling listActivatedSystems API function.
        """
        shell.help_activationkey_listsystems = MagicMock()
        shell.client.activationkey.listActivatedSystems = MagicMock(
            return_value=[
                {"hostname": "chair.lan"},
                {"hostname": "houseshoe.lan"},
            ]
        )

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint) as mpr:
            spacecmd.activationkey.do_activationkey_listsystems(shell, "key")
        assert not shell.help_activationkey_listsystems.called
        assert shell.client.activationkey.listActivatedSystems.called
        assert mprint.called
        assert mprint.call_args_list[0][0][0] == 'chair.lan\nhouseshoe.lan'

    def test_do_activationkey_details_noargs(self, shell):
        """
        Test activationkey_details shows a help screen if no sufficient arguments has been passed.
        """
        shell.help_activationkey_details = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()
        shell.client.activationkey.listConfigChannels = MagicMock()
        shell.client.activationkey.checkConfigDeployment = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_listsystems(shell, "")
        assert shell.help_activationkey_listsystems.called
        assert not shell.client.activationkey.getDetails.called
        assert not shell.client.systemgroup.getDetails.called
        assert not shell.client.activationkey.listConfigChannels.called
        assert not shell.client.activationkey.checkConfigDeployment.called

    def test_do_activationkey_details_args(self, shell):
        """
        Test activationkey_details returns key details if proper arguments passed.
        """
        shell.help_activationkey_details = MagicMock()

        shell.client.activationkey.getDetails = MagicMock(
            return_value={
                "key": "somekey",
                "description": "Key description",
                "universal_default": "yes",
                "usage_limit": "42",
                "contact_method": "230V/AC",
                "server_group_ids": ["a", "b", "c"],
                "child_channel_labels": ["child_channel_1", "child_channel_2"],
                "entitlements": ["entitlement_1", "entitlement_2"],
                "packages": [{"name": "emacs"}],
                "base_channel_label": "base_channel",
            }
        )
        shell.client.activationkey.listConfigChannels = MagicMock(
            return_value=[
                {"label": "somekey_config_channel_1"},
                {"label": "somekey_config_channel_2"},
            ]
        )
        shell.client.activationkey.checkConfigDeployment = MagicMock(return_value=0)
        shell.client.systemgroup.getDetails = MagicMock(
            side_effect=[
                {"name": "group_a"},
                {"name": "group_b"},
                {"name": "group_c"},
            ]
        )

        mprint = MagicMock()
        with patch("spacecmd.activationkey.print", mprint) as mpr:        
            out = spacecmd.activationkey.do_activationkey_details(shell, "somekey")
        assert not mprint.called

        expectation = ['Key:                    somekey',
                       'Description:            Key description', 'Universal Default:      yes',
                       'Usage Limit:            42', 'Deploy Config Channels: False',
                       'Contact Method:         230V/AC', '', 'Software Channels', '-----------------',
                       'base_channel', ' |-- child_channel_1', ' |-- child_channel_2', '',
                       'Configuration Channels', '----------------------', 'somekey_config_channel_1',
                       'somekey_config_channel_2', '', 'Entitlements', '------------',
                       'entitlement_1\nentitlement_2', '', 'System Groups', '-------------',
                       'group_a\ngroup_b\ngroup_c', '', 'Packages', '--------', 'emacs']
        assert len(out) == len(expectation)
        for idx, line in enumerate(out):
            assert line == expectation[idx]
    
    def test_do_activationkey_enableconfigdeployment_noargs(self, shell):
        """
        Test activationkey_enableconfigdeployment command is invoking help message on insufficient arguments.
        """
        shell.help_activationkey_enableconfigdeployment = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()

        spacecmd.activationkey.do_activationkey_enableconfigdeployment(shell, "")
        assert shell.help_activationkey_enableconfigdeployment.called
        assert not shell.client.activationkey.enableConfigDeployment.called

    def test_do_activationkey_enableconfigdeployment_args(self, shell):
        """
        Test activationkey_enableconfigdeployment command is invoking enableConfigDeployment API call.
        """
        shell.help_activationkey_enableconfigdeployment = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_enableconfigdeployment(shell, "foo bar baz")

        assert logger.debug.called
        assert logger.debug.call_args_list[0][0][0] == "Enabling config file deployment for foo"
        assert logger.debug.call_args_list[1][0][0] == "Enabling config file deployment for bar"
        assert logger.debug.call_args_list[2][0][0] == "Enabling config file deployment for baz"

        keynames = ["foo", "bar", "baz"]
        assert shell.client.activationkey.enableConfigDeployment.called
        for call in shell.client.activationkey.enableConfigDeployment.call_args_list:
            session, keyname = call[0]
            assert shell.session == session
            assert keyname in keynames
            keynames.pop(keynames.index(keyname))
        assert keynames == []

    def test_do_activationkey_disableconfigdeployment_noargs(self, shell):
        """
        Test activationkey_disableconfigdeployment command is invoking help message on insufficient arguments.
        """
        shell.help_activationkey_disableconfigdeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()

        spacecmd.activationkey.do_activationkey_disableconfigdeployment(shell, "")
        assert shell.help_activationkey_disableconfigdeployment.called
        assert not shell.client.activationkey.disableConfigDeployment.called

    def test_do_activationkey_disableconfigdeployment_args(self, shell):
        """
        Test activationkey_disableconfigdeployment command is invoking disableConfigDeployment API call.
        """
        shell.help_activationkey_disableconfigdeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_disableconfigdeployment(shell, "foo bar baz")

        assert logger.debug.called
        assert logger.debug.call_args_list[0][0][0] == "Disabling config file deployment for foo"
        assert logger.debug.call_args_list[1][0][0] == "Disabling config file deployment for bar"
        assert logger.debug.call_args_list[2][0][0] == "Disabling config file deployment for baz"

        keynames = ["foo", "bar", "baz"]
        assert shell.client.activationkey.disableConfigDeployment.called
        for call in shell.client.activationkey.disableConfigDeployment.call_args_list:
            session, keyname = call[0]
            assert shell.session == session
            assert keyname in keynames
            keynames.pop(keynames.index(keyname))
        assert keynames == []

    def test_do_activationkey_addconfigchannels_setbasechannel_noargs(self, shell):
        """
        Test do_activationkey_addconfigchannels_setbasechannel involes help message on issuficient arguments.
        """
        shell.help_activationkey_setbasechannel = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setbasechannel(shell, "")
        assert shell.help_activationkey_setbasechannel.called
        assert not shell.client.activationkey.setDetails.called
        assert not shell.client.activationkey.getDetails.called

        spacecmd.activationkey.do_activationkey_setbasechannel(shell, "key")
        assert shell.help_activationkey_setbasechannel.called
        assert not shell.client.activationkey.setDetails.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_addconfigchannels_setbasechannel_args(self, shell):
        """
        Test do_activationkey_addconfigchannels_setbasechannel calls setDetails API call on proper args.
        """
        key_details = {
            "base_channel_label": "death_star_channel",
            "description": "Darth Vader's base channel",
            "usage_limit": 42,
            "universal_default": True,
        }
        shell.help_activationkey_setbasechannel = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value=key_details)

        spacecmd.activationkey.do_activationkey_setbasechannel(shell, "red_key death_star_patches_channel")
        session, keyname, details = shell.client.activationkey.setDetails.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "red_key"

        for dkey in ["description", "usage_limit", "universal_default"]:
            assert dkey in details
            assert key_details[dkey] == details[dkey]
        assert details["base_channel_label"] == "death_star_patches_channel"

    def test_do_activationkey_addconfigchannels_setbasechannel_usage_limit(self, shell):
        """
        Test do_activationkey_addconfigchannels_setbasechannel resets usage limit from 0 to -1.
        """
        key_details = {
            "base_channel_label": "death_star_channel",
            "description": "Darth Vader's base channel",
            "usage_limit": 0,
            "universal_default": True,
        }
        shell.help_activationkey_setbasechannel = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value=key_details)

        spacecmd.activationkey.do_activationkey_setbasechannel(shell, "red_key death_star_patches_channel")
        session, keyname, details = shell.client.activationkey.setDetails.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "red_key"

        for dkey in ["description", "universal_default"]:
            assert dkey in details
            assert key_details[dkey] == details[dkey]
        assert details["base_channel_label"] == "death_star_patches_channel"
        assert details["usage_limit"] == -1

    def test_do_activationkey_addconfigchannels_setusagelimit_noargs(self, shell):
        """
        Test do_activationkey_addconfigchannels_setusagelimit involes help message on issuficient arguments.
        """
        shell.help_activationkey_setusagelimit = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setusagelimit(shell, "")
        assert shell.help_activationkey_setusagelimit.called
        assert not shell.client.activationkey.setDetails.called
        assert not shell.client.activationkey.getDetails.called

        spacecmd.activationkey.do_activationkey_setusagelimit(shell, "key")
        assert shell.help_activationkey_setusagelimit.called
        assert not shell.client.activationkey.setDetails.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_addconfigchannels_setusagelimit_args(self, shell):
        """
        Test do_activationkey_addconfigchannels_setbasechannel resets usage limit from 0 to -1.
        """
        key_details = {
            "base_channel_label": "death_star_channel",
            "description": "Darth Vader's base channel",
            "usage_limit": 0,
            "universal_default": True,
        }
        shell.help_activationkey_setusagelimit = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value=key_details)

        spacecmd.activationkey.do_activationkey_setusagelimit(shell, "red_key 42")
        session, keyname, details = shell.client.activationkey.setDetails.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "red_key"

        for dkey in ["description", "universal_default", "base_channel_label"]:
            assert dkey in details
            assert key_details[dkey] == details[dkey]
        assert type(details["usage_limit"]) == int
        assert details["usage_limit"] == 42

    def test_do_activationkey_addconfigchannels_setuniversaldefault_noargs(self, shell):
        """
        Test do_activationkey_addconfigchannels_setuniversaldefault involes help message on issuficient arguments.
        """
        shell.help_activationkey_setuniversaldefault = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setuniversaldefault(shell, "")
        assert shell.help_activationkey_setuniversaldefault.called
        assert not shell.client.activationkey.setDetails.called
        assert not shell.client.activationkey.getDetails.called

    def test_do_activationkey_addconfigchannels_setuniversaldefault_args(self, shell):
        """
        Test do_activationkey_addconfigchannels_setuniversaldefault calls API to set the key settings.
        """
        key_details = {
            "base_channel_label": "death_star_channel",
            "description": "Darth Vader's base channel",
            "usage_limit": 42,
            "universal_default": False,
        }
        shell.help_activationkey_setuniversaldefault = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value=key_details)

        spacecmd.activationkey.do_activationkey_setuniversaldefault(shell, "red_key 42")
        session, keyname, details = shell.client.activationkey.setDetails.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "red_key"

        for dkey in ["description", "usage_limit", "base_channel_label"]:
            assert dkey in details
            assert key_details[dkey] == details[dkey]
        assert type(details["universal_default"]) == bool
        assert details["universal_default"]

    def test_export_activationkey_getdetails_exception_handling(self, shell):
        """
        Test export_activationkey_getdetails XMLRPC failure.
        """
    
        key_details = {
            "server_group_ids": [],
        }
        logger = MagicMock()
        shell.client.activationkey.getDetails = MagicMock()
        shell.client.activationkey.listConfigChannels = MagicMock(side_effect=xmlrpclib.Fault("boom", "box"))
        shell.client.activationkey.checkConfigDeployment = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value=key_details)

        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.export_activationkey_getdetails(shell, "")
        assert logger.debug.call_args_list[1][0][0] == ("activationkey.listConfigChannel threw an exeception, "
                                                        "setting config_channels=False")
        
    def test_export_activationkey_getdetails(self, shell):
        """
        Test export_activationkey_getdetails.
        """

        key_details = {
            "server_group_ids": [1, 2, 3],
        }

        gr_details = [
            {"name": "first"},
            {"name": "second"},
            {"name": "third"},
        ]
        logger = MagicMock()
        shell.client.activationkey.getDetails = MagicMock(return_value=key_details)
        shell.client.activationkey.listConfigChannels = MagicMock(
            return_value=[
                {"label": "rd2d_patches_channel"},
                {"label": "k_2so_firmware_channel"},
            ]
        )
        shell.client.activationkey.checkConfigDeployment = MagicMock(return_value=True)
        shell.client.systemgroup.getDetails = MagicMock(side_effect=gr_details)

        with patch("spacecmd.activationkey.logging", logger):
            out = spacecmd.activationkey.export_activationkey_getdetails(shell, "red_key")

        {
            'server_group_ids': [1, 2, 3],
            'config_channels': ['rd2d_patches_channel',
                                'k_2so_firmware_channel'],
            'config_deploy': True,
            'server_groups': ['first', 'second', 'third']
        }

        assert "server_group_ids" in out
        assert out["server_group_ids"] == [1, 2, 3]
        assert "config_channels" in out
        for cfg_channel in ['rd2d_patches_channel',
                            'k_2so_firmware_channel']:
            assert cfg_channel in out["config_channels"]
        assert "config_deploy" in out
        assert out["config_deploy"]
        assert "server_groups" in out
        assert len(out["server_groups"]) == 3
        for srv_group in ['first', 'second', 'third']:
            assert srv_group in out["server_groups"]

    @patch("spacecmd.activationkey.json_dump_to_file", MagicMock())
    @patch("spacecmd.activationkey.os.path.isfile", MagicMock(return_value=False))
    def test_do_activationkey_export_noarg(self, shell):
        """
        Test do_activationkey_export exports to 'akey_all.json' if not specified otherwise.
        """
        logger = MagicMock()
        shell.do_activationkey_list = MagicMock(return_value=["one", "two", "three"])
        shell.export_activationkey_getdetails = MagicMock(side_effect=[{}, {}, {}])
        shell.client.activationkey.checkConfigDeployment = MagicMock(return_value=True)

        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_export(shell, "")

        assert logger.debug.called
        assert logger.error.called
        assert logger.info.called

        assert logger.error.call_args_list[0][0][0] == "Failed to save exported keys to file: akey_all.json"
        assert logger.debug.call_args_list[0][0][0] == "About to dump 3 keys to akey_all.json"

        expectation = [
            'Exporting ALL activation keys to akey_all.json',
            'Exporting key one to akey_all.json',
            'Exporting key two to akey_all.json',
            'Exporting key three to akey_all.json',
        ]
        for idx, call in enumerate(logger.info.call_args_list):
            assert call[0][0] == expectation[idx]

    @patch("spacecmd.activationkey.json_dump_to_file", MagicMock())
    @patch("spacecmd.activationkey.os.path.isfile", MagicMock(return_value=False))
    def test_do_activationkey_export_filename_arg(self, shell):
        """
        Test do_activationkey_export exports to 'akey_all.json' if not specified otherwise.
        """
        logger = MagicMock()
        shell.do_activationkey_list = MagicMock(return_value=["one", "two", "three"])
        shell.export_activationkey_getdetails = MagicMock(side_effect=[{}, {}, {}])
        shell.client.activationkey.checkConfigDeployment = MagicMock(return_value=True)

        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_export(shell, "-f somefile.json")

        assert logger.debug.called
        assert logger.error.called
        assert logger.info.called

        assert logger.error.call_args_list[0][0][0] == "Failed to save exported keys to file: somefile.json"
        assert logger.debug.call_args_list[0][0][0] == "Passed filename do_activationkey_export somefile.json"

        expectation = [
            'Exporting ALL activation keys to somefile.json',
            'Exporting key one to somefile.json',
            'Exporting key two to somefile.json',
            'Exporting key three to somefile.json',
        ]
        for idx, call in enumerate(logger.info.call_args_list):
            assert call[0][0] == expectation[idx]

    def test_do_activationkey_import_noargs(self, shell):
        """
        Test activationkey_import command is invoking help message on insufficient arguments.
        """
        shell.help_activationkey_import = MagicMock()
        shell.import_activationkey_fromdetails = MagicMock(return_value=False)

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            spacecmd.activationkey.do_activationkey_import(shell, "")

        assert shell.help_activationkey_import.called
        assert not shell.import_activationkey_fromdetails.called
        assert logger.error.called
        assert logger.error.call_args_list[0][0][0] == 'No filename passed'

    def test_do_activationkey_fromdetails_existingkey(self, shell):
        """
        Test import_activationkey_fromdetails does not import anything if key already exist.
        """
        shell.client.activationkey.create = MagicMock()
        shell.do_activationkey_list = MagicMock(return_value=["somekey"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(shell, {"key": "somekey"})
        assert not shell.client.activationkey.create.called
        assert shell.do_activationkey_list.called
        assert not shell.client.activationkey.addChildChannels.called
        assert not shell.client.activationkey.enableConfigDeployment.called
        assert not shell.client.activationkey.disableConfigDeployment.called
        assert not shell.client.activationkey.addConfigChannels.called
        assert not shell.client.activationkey.addServerGroups.called
        assert not shell.client.activationkey.addServerGroups.called
        assert not shell.client.activationkey.addPackages.called

        assert logger.warning.called
        assert logger.warning.call_args_list[0][0][0] == 'somekey already exists! Skipping!'
        assert type(ret) == bool
        assert not ret

    def test_do_activationkey_fromdetails_newkey_no_usage_limit(self, shell):
        """
        Test import_activationkey_fromdetails no usage limit.
        """
        shell.client.activationkey.create = MagicMock(return_value={})
        shell.do_activationkey_list = MagicMock(return_value=["some_existing_key"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(
                shell,
                {
                    "key": "somekey",
                    "usage_limit": 0,
                    "base_channel_label": "none",
                    "description": "Key description",
                    "entitlements": ["one", "two", "three"],
                    "universal_default": True,
                }
            )
        assert shell.do_activationkey_list.called
        assert shell.client.activationkey.create.called
        assert not shell.client.activationkey.addChildChannels.called
        assert not shell.client.activationkey.enableConfigDeployment.called
        assert not shell.client.activationkey.disableConfigDeployment.called
        assert not shell.client.activationkey.addConfigChannels.called
        assert not shell.client.activationkey.addServerGroups.called
        assert not shell.client.activationkey.addServerGroups.called
        assert not shell.client.activationkey.addPackages.called
        assert logger.debug.call_args_list[0][0][0] == "Found key somekey, importing as somekey"

        assert len(shell.client.activationkey.create.call_args_list[0][0]) == 6
        session, keyname, descr, basech, entl, udef = shell.client.activationkey.create.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "somekey"
        assert descr == "Key description"
        assert basech == ""
        assert entl == ["one", "two", "three"]
        assert udef

    def test_do_activationkey_fromdetails_newkey_fixed_usage_limit(self, shell):
        """
        Test import_activationkey_fromdetails usage limit given.
        """
        shell.client.activationkey.create = MagicMock(return_value={})
        shell.do_activationkey_list = MagicMock(return_value=["some_existing_key"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(
                shell,
                {
                    "key": "somekey",
                    "usage_limit": 42,
                    "base_channel_label": "none",
                    "description": "Key description",
                    "entitlements": ["one", "two", "three"],
                    "universal_default": True,
                }
            )
        assert len(shell.client.activationkey.create.call_args_list[0][0]) == 7
        session, keyname, descr, basech, ulim, entl, udef = shell.client.activationkey.create.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "somekey"
        assert descr == "Key description"
        assert basech == ""
        assert ulim == 42
        assert entl == ["one", "two", "three"]
        assert udef

        # This is expected: see mock return on API call "create".
        assert logger.error.call_args_list[0][0][0] == 'Failed to import key somekey'

    def test_do_activationkey_fromdetails_no_cfgdeploy(self, shell):
        """
        Test import_activationkey_fromdetails no configuration deployment.
        """
        shell.client.activationkey.create = MagicMock(return_value={"name": "whatever"})
        shell.do_activationkey_list = MagicMock(return_value=["some_existing_key"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value=None)
        shell.client.systemgroup.create = MagicMock(
            side_effect=[
                {"id": 1},
                {"id": 2},
                {"id": 3},
            ]
        )

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(
                shell,
                {
                    "key": "somekey",
                    "usage_limit": 42,
                    "base_channel_label": "none",
                    "description": "Key description",
                    "entitlements": ["one", "two", "three"],
                    "universal_default": True,
                    "child_channel_labels": "child_channel_labels",
                    "config_channels": ["config_channel"],
                    "server_groups": ["one", "two", "three"],
                    "packages": ["emacs"],
                    "config_deploy": 0,
                }
            )
        assert not shell.client.activationkey.enableConfigDeployment.called
        assert shell.client.activationkey.disableConfigDeployment.called
        session, keydata = shell.client.activationkey.disableConfigDeployment.call_args_list[0][0]
        assert shell.session == session
        assert keydata == shell.client.activationkey.create()

    def test_do_activationkey_fromdetails_cfgdeploy(self, shell):
        """
        Test import_activationkey_fromdetails configuration deployment was set.
        """
        shell.client.activationkey.create = MagicMock(return_value={"name": "whatever"})
        shell.do_activationkey_list = MagicMock(return_value=["some_existing_key"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value=None)
        shell.client.systemgroup.create = MagicMock(
            side_effect=[
                {"id": 1},
                {"id": 2},
                {"id": 3},
            ]
        )

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(
                shell,
                {
                    "key": "somekey",
                    "usage_limit": 42,
                    "base_channel_label": "none",
                    "description": "Key description",
                    "entitlements": ["one", "two", "three"],
                    "universal_default": True,
                    "child_channel_labels": "child_channel_labels",
                    "config_channels": ["config_channel"],
                    "server_groups": ["one", "two", "three"],
                    "packages": ["emacs"],
                    "config_deploy": 1,
                }
            )
        assert shell.client.activationkey.enableConfigDeployment.called
        assert not shell.client.activationkey.disableConfigDeployment.called
        session, keydata = shell.client.activationkey.enableConfigDeployment.call_args_list[0][0]
        assert shell.session == session
        assert keydata == shell.client.activationkey.create()

    def test_do_activationkey_fromdetails_groups_pkgs(self, shell):
        """
        Test import_activationkey_fromdetails groups and packages processed.
        """
        shell.client.activationkey.create = MagicMock(return_value={"name": "whatever"})
        shell.do_activationkey_list = MagicMock(return_value=["some_existing_key"])
        shell.client.activationkey.addChildChannels = MagicMock()
        shell.client.activationkey.enableConfigDeployment = MagicMock()
        shell.client.activationkey.disableConfigDeployment = MagicMock()
        shell.client.activationkey.addConfigChannels = MagicMock()
        shell.client.activationkey.addServerGroups = MagicMock()
        shell.client.activationkey.addPackages = MagicMock()
        shell.client.systemgroup.getDetails = MagicMock(return_value=None)
        shell.client.systemgroup.create = MagicMock(
            side_effect=[
                {"id": 1},
                {"id": 2},
                {"id": 3},
            ]
        )

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.import_activationkey_fromdetails(
                shell,
                {
                    "key": "somekey",
                    "usage_limit": 42,
                    "base_channel_label": "none",
                    "description": "Key description",
                    "entitlements": ["one", "two", "three"],
                    "universal_default": True,
                    "child_channel_labels": "child_channel_labels",
                    "config_channels": ["config_channel"],
                    "server_groups": ["one", "two", "three"],
                    "packages": ["emacs"],
                    "config_deploy": 1,
                }
            )
        session, keyprm, gids = shell.client.activationkey.addServerGroups.call_args_list[0][0]
        assert shell.session == session
        assert keyprm == {'name': 'whatever'}
        assert gids == [1, 2, 3]

        session, keyprm, pkgs = shell.client.activationkey.addPackages.call_args_list[0][0]
        assert pkgs == ["emacs"]

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_noargs(self, shell):
        """
        Test do_activationkey_clone no arguments requires some.
        """
        logger = MagicMock()
        shell.do_activationkey_list = MagicMock()
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.do_activationkey_clone(shell, "")

        assert logger.error.called
        assert shell.help_activationkey_clone.called
        assert shell.do_activationkey_list.called
        assert not shell.export_activationkey_getdetails.called
        assert not shell.list_base_channels.called
        assert not shell.list_child_channels.called
        assert not shell.do_configchannel_list.called
        assert not shell.import_activtionkey_fromdetails.called
        assert ret is None
        assert logger.error.call_args_list[0][0][0] == 'Error - must specify either -c or -x options!'

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_wrongargs(self, shell):
        """
        Test do_activationkey_clone wrong arguments prompts for correction.
        """
        shell.do_activationkey_list = MagicMock()
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        with pytest.raises(Exception) as exc:
            spacecmd.activationkey.do_activationkey_clone(shell, "--nonsense=true")

        assert "Exception: unrecognized arguments: --nonsense=true" in str(exc)

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_existing_clone_arg(self, shell):
        """
        Test do_activationkey_clone existing clone name passed to the arguments.
        """
        shell.do_activationkey_list = MagicMock(return_value=["key_clone_name"])
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.do_activationkey_clone(shell, "-c key_clone_name")

        assert logger.error.called
        assert not shell.help_activationkey_clone.called
        assert shell.do_activationkey_list.called
        assert not shell.export_activationkey_getdetails.called
        assert not shell.list_base_channels.called
        assert not shell.list_child_channels.called
        assert not shell.do_configchannel_list.called
        assert not shell.import_activtionkey_fromdetails.called
        assert logger.error.call_args_list[0][0][0] == "Key key_clone_name already exists"
        assert ret is None

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_noargs_to_clone(self, shell):
        """
        Test do_activationkey_clone no arguments to a clone name passed to the arguments.
        """
        shell.do_activationkey_list = MagicMock(return_value=["key_some_clone_name"])
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.do_activationkey_clone(shell, "-c key_clone_name")

        assert logger.error.called
        assert shell.help_activationkey_clone.called
        assert shell.do_activationkey_list.called
        assert not shell.export_activationkey_getdetails.called
        assert not shell.list_base_channels.called
        assert not shell.list_child_channels.called
        assert not shell.do_configchannel_list.called
        assert not shell.import_activtionkey_fromdetails.called
        assert logger.error.call_args_list[0][0][0] == "Error no activationkey to clone passed!"
        assert ret is None

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_keyargs_to_clone_filtered(self, shell):
        """
        Test do_activationkey_clone filtered out keys.
        """
        shell.do_activationkey_list = MagicMock(return_value=["key_some_clone_name"])
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.do_activationkey_clone(shell, "orig_key -c key_clone_name")

        assert not logger.error.called
        assert not shell.help_activationkey_clone.called
        assert shell.do_activationkey_list.called
        assert not shell.export_activationkey_getdetails.called
        assert not shell.list_base_channels.called
        assert not shell.list_child_channels.called
        assert not shell.do_configchannel_list.called
        assert not shell.import_activtionkey_fromdetails.called

        expectations = [
            "Got args=['orig_key'] 1",
            "Filtered akeys []",
            "all akeys ['key_some_clone_name']",
        ]
        for idx, call in enumerate(logger.debug.call_args_list):
            assert call[0][0] == expectations[idx]

    @patch("spacecmd.activationkey.is_interactive", MagicMock(return_value=False))
    @patch("spacecmd.activationkey.prompt_user", MagicMock(side_effect=["original_key", "cloned_key"]))
    def test_do_activationkey_clone_keyargs_unfiltered(self, shell):
        """
        Test do_activationkey_clone not filtered out keys.
        """
        shell.do_activationkey_list = MagicMock(return_value=["key_some_clone_name"])
        shell.help_activationkey_clone = MagicMock()
        shell.export_activationkey_getdetails = MagicMock()
        shell.list_base_channels = MagicMock()
        shell.list_child_channels = MagicMock()
        shell.do_configchannel_list = MagicMock()
        shell.import_activtionkey_fromdetails = MagicMock()

        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            ret = spacecmd.activationkey.do_activationkey_clone(shell, "key_some* -c key_clone_name")
        expectations = [
            "Got args=['key_some.*'] 1",
            "Filtered akeys ['key_some_clone_name']",
            "all akeys ['key_some_clone_name']",
            "Cloning key_some_clone_name",
        ]
        for idx, call in enumerate(logger.debug.call_args_list):
            assert call[0][0] == expectations[idx]

    def test_check_activationkey_nokey(self, shell):
        """
        Test check activation key helper returns False on no key.
        """
        shell.is_activationkey = MagicMock(return_value=False)
        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            assert not spacecmd.activationkey.check_activationkey(shell, "")
        assert logger.error.called
        assert logger.error.call_args_list[0][0][0] == 'no activationkey label given'

    def test_check_activationkey_not_a_key(self, shell):
        """
        Test check activation key helper returns False on not a key.
        """
        shell.is_activationkey = MagicMock(return_value=False)
        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            assert not spacecmd.activationkey.check_activationkey(shell, "some_not_a_key")
        assert logger.error.called
        assert logger.error.call_args_list[0][0][0] == 'invalid activationkey label some_not_a_key'

    def test_check_activationkey_correct_key(self, shell):
        """
        Test check activation key helper returns True if a key is correct.
        """
        shell.is_activationkey = MagicMock(return_value=True)
        logger = MagicMock()
        with patch("spacecmd.activationkey.logging", logger):
            assert spacecmd.activationkey.check_activationkey(shell, "some_not_a_key")
        assert not logger.error.called

    def test_activationkey_diff_noarg(self, shell):
        """
        Test dump activation key helper invokes help message on insufficient arguments.
        """
        for args in ["", "one two three", "some more args here"]:
            shell.help_activationkey_diff = MagicMock()
            shell.dump_activationkey = MagicMock()
            shell.check_activationkey = MagicMock()
            shell.do_activationkey_getcorresponding = MagicMock()

            _diff = MagicMock()
            with patch("spacecmd.activationkey.diff", _diff):
                spacecmd.activationkey.do_activationkey_diff(shell, "")

            assert shell.help_activationkey_diff.called
            assert not shell.dump_activationkey.called
            assert not shell.check_activationkey.called
            assert not shell.do_activationkey_getcorresponding.called
            assert not _diff.called

    def test_activationkey_diff_arg(self, shell):
        """
        Test dump activation key helper invokes expected sequence of function calls.
        """
        shell.help_activationkey_diff = MagicMock()
        shell.dump_activationkey = MagicMock()
        shell.check_activationkey = MagicMock()
        shell.do_activationkey_getcorresponding = MagicMock()

        _diff = MagicMock()
        with patch("spacecmd.activationkey.diff", _diff):
            spacecmd.activationkey.do_activationkey_diff(shell, "some_key")

        assert not shell.help_activationkey_diff.called
        assert shell.dump_activationkey.called
        assert shell.check_activationkey.called
        assert shell.do_activationkey_getcorresponding.called
        assert _diff.called

    def test_activationkey_diff(self, shell):
        """
        Test dump activation key helper returns key diffs.
        """
        shell.help_activationkey_diff = MagicMock()
        shell.dump_activationkey = MagicMock(side_effect=[["some data"], ["some data again"]])
        shell.check_activationkey = MagicMock(return_value=True)
        shell.do_activationkey_getcorresponding = MagicMock(return_value="some_channel")

        gsdd = MagicMock(return_value=({"one": "two", "three": "three"}, {"one": "two", "three": "four"}))
        with patch("spacecmd.activationkey.get_string_diff_dicts", gsdd):
            out = spacecmd.activationkey.do_activationkey_diff(shell, "some_key")
        assert out == ['--- some_key\n', '+++ some_channel\n', '@@ -1 +1 @@\n', '-some data', '+some data again']
    
    def test_do_activationkey_diable_noargs(self, shell):
        """
        Test do_activationkey_disable command triggers help on no args.
        """
        shell.help_activationkey_disable = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_disable(shell, "")
        assert shell.help_activationkey_disable.called
        assert not shell.client.activationkey.setDetails.called

    def test_do_activationkey_diable_args(self, shell):
        """
        Test do_activationkey_disable command triggers activationkey.setDetails API call.
        """
        keys = ["key_one", "key_two"]
        shell.help_activationkey_disable = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.do_activationkey_list = MagicMock(return_value=keys)

        spacecmd.activationkey.do_activationkey_disable(shell, "key_*")
        assert not shell.help_activationkey_disable.called
        assert shell.client.activationkey.setDetails.called

        for call in shell.client.activationkey.setDetails.call_args_list:
            session, key, arg = call[0]
            assert shell.session == session
            assert key in keys
            assert "disabled" in arg
            assert arg["disabled"]

    def test_do_activationkey_enable_noargs(self, shell):
        """
        Test do_activationkey_enable command triggers help on no args.
        """
        shell.help_activationkey_enable = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_enable(shell, "")
        assert shell.help_activationkey_enable.called
        assert not shell.client.activationkey.setDetails.called

    def test_do_activationkey_enable_args(self, shell):
        """
        Test do_activationkey_enable command triggers activationkey.setDetails API call.
        """
        keys = ["key_one", "key_two"]
        shell.help_activationkey_enable = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()
        shell.do_activationkey_list = MagicMock(return_value=keys)

        spacecmd.activationkey.do_activationkey_enable(shell, "key_*")
        assert not shell.help_activationkey_enable.called
        assert shell.client.activationkey.setDetails.called

        for call in shell.client.activationkey.setDetails.call_args_list:
            session, key, arg = call[0]
            assert shell.session == session
            assert key in keys
            assert "disabled" in arg
            assert not arg["disabled"]

    def test_do_activationkey_setdescription_noargs(self, shell):
        """
        Test do_activationkey_setdescription command triggers help on no args.
        """
        shell.help_activationkey_setdescription = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setdescription(shell, "")
        assert shell.help_activationkey_setdescription.called
        assert not shell.client.activationkey.setDetails.called

    def test_do_activationkey_setdescription_args(self, shell):
        """
        Test do_activationkey_setdescription command triggers activationkey.setDetails API call.
        """
        shell.help_activationkey_enable = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setdescription(shell, "key_one some description of it here")
        assert not shell.help_activationkey_enable.called
        assert shell.client.activationkey.setDetails.called

        for call in shell.client.activationkey.setDetails.call_args_list:
            session, key, arg = call[0]
            assert shell.session == session
            assert "description" in arg
            assert arg["description"] == "some description of it here"

    def test_do_activationkey_setcontactmethod_noargs(self, shell):
        """
        Test do_activationkey_setcontactmethod command triggers help on no args.
        """
        shell.help_activationkey_setcontactmethod = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setcontactmethod(shell, "")
        assert shell.help_activationkey_setcontactmethod.called
        assert not shell.client.activationkey.setDetails.called

    def test_do_activationkey_setcontactmethod_args(self, shell):
        """
        Test do_activationkey_setdescription command triggers activationkey.setDetails API call.
        """
        shell.help_activationkey_setcontactmethod = MagicMock()
        shell.client.activationkey.setDetails = MagicMock()

        spacecmd.activationkey.do_activationkey_setcontactmethod(shell, "key_one 230V")
        assert not shell.help_activationkey_setcontactmethod.called
        assert shell.client.activationkey.setDetails.called

        for call in shell.client.activationkey.setDetails.call_args_list:
            session, key, arg = call[0]
            assert shell.session == session
            assert "contact_method" in arg
            assert arg["contact_method"] == "230V"

    
07070100000030000081B40000000000000000000000015DA8415F00000A84000000000000000000000000000000000000001B00000000spacecmd/tests/test_api.py# coding: utf-8
"""
Test suite for spacecmd.api
"""
from mock import MagicMock, patch, mock_open
from spacecmd import api
import helpers


class TestSCAPI:
    """
    Test class for testing spacecmd API.
    """
    def test_no_args(self):
        """
        Test calling API without any arguments.
        """
        shell = MagicMock()
        shell.help_api = MagicMock()
        api.do_api(shell, "")
        assert shell.help_api.called

    def test_args_output(self):
        """
        Test output option.
        """
        shell = MagicMock()
        shell.help_api = MagicMock()
        shell.client = MagicMock()
        shell.client.call = MagicMock(return_value=["one", "two", "three"])

        log = MagicMock()
        out = helpers.FileHandleMock()
        with patch("spacecmd.api.open", out, create=True) as mop, \
             patch("spacecmd.api.logging", log) as mlog:
            api.do_api(shell, "call -o /tmp/spacecmd.log")

        assert not mlog.warning.called
        assert out.get_content() == '[\n  "one",\n  "two",\n  "three"\n]'
        assert out.get_init_kwargs() == {}
        assert out.get_init_args() == ('/tmp/spacecmd.log', 'w')
        assert out._closed

    def test_args_format(self):
        """
        Test format option.
        """
        shell = MagicMock()
        shell.help_api = MagicMock()
        shell.client = MagicMock()
        shell.client.call = MagicMock(return_value=["one", "two", "three"])

        log = MagicMock()
        out = helpers.FileHandleMock()
        with patch("spacecmd.api.open", out, create=True) as mop, \
             patch("spacecmd.api.logging", log) as mlog:
            api.do_api(shell, "call -o /tmp/spacecmd.log -F '>>> %s'")

        assert not mlog.warning.called
        assert out.get_content() == '>>> one\n>>> two\n>>> three\n'
        assert out.get_init_kwargs() == {}
        assert out.get_init_args() == ('/tmp/spacecmd.log', 'w')
        assert out._closed

    def test_args_args(self):
        """
        Test args option.
        """
        shell = MagicMock()
        shell.help_api = MagicMock()
        shell.client = MagicMock()
        shell.client.call = MagicMock(return_value=["one", "two", "three"])
        shell.session = "session"

        log = MagicMock()
        out = helpers.FileHandleMock()
        with patch("spacecmd.api.open", out, create=True) as mop, \
             patch("spacecmd.api.logging", log) as mlog:
            api.do_api(shell, "call -A first,second,123 -o /tmp/spacecmd.log")
        assert shell.client.call.called
        assert shell.client.call.call_args_list[0][0] == ('session', 'first', 'second', 123)
        assert out._closed
07070100000031000081B40000000000000000000000015DA8415F000001FF000000000000000000000000000000000000002600000000spacecmd/tests/test_argumentparser.py# coding: utf-8
"""
Test argument parser.
"""
import pytest
import spacecmd.argumentparser


class TestSCArgumentParser:
    """
    Test argument parser subclass.
    """
    def test_argparse_raise_exception(self):
        """
        Test argparse raise exception.
        """
        msg = "not enough memory, get system upgrade"
        argparse = spacecmd.argumentparser.SpacecmdArgumentParser()
        with pytest.raises(Exception) as exc:
            argparse.error(msg)
        assert msg in str(exc)
07070100000032000081B40000000000000000000000015DA8415F0000403B000000000000000000000000000000000000002100000000spacecmd/tests/test_cryptokey.py# coding: utf-8
"""
Test suite for cryptokey.
"""
from mock import MagicMock, patch
import pytest
from xmlrpc import client as xmlrpclib
import spacecmd.cryptokey
from helpers import shell, assert_expect


class TestSCCryptokey:
    """
    Test cryptokey API.
    """
    def test_cryptokey_create_no_keytype(self, shell):
        """
        Test do_cryptokey_create without correct key type.

        :param shell:
        :return:
        """
        shell.help_cryptokey_create = MagicMock()
        shell.client.kickstart.keys.create = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        read_file = MagicMock(return_value="contents")
        prompt_user = MagicMock(side_effect=["", "interactive descr", "/tmp/file.txt"])
        editor = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.prompt_user", prompt_user) as pmu, \
            patch("spacecmd.cryptokey.read_file", read_file) as rfl, \
            patch("spacecmd.cryptokey.editor", editor) as edt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_create(shell, "")

        assert not shell.help_cryptokey_create.called
        assert not shell.client.kickstart.keys.create.called
        assert not editor.called
        assert read_file.called
        assert prompt_user.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "Invalid key type")

    def test_cryptokey_create_interactive_no_contents(self, shell):
        """
        Test do_cryptokey_create without arguments (interactive, no contents given).

        :param shell:
        :return:
        """
        shell.help_cryptokey_create = MagicMock()
        shell.client.kickstart.keys.create = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        read_file = MagicMock()
        prompt_user = MagicMock(side_effect=["g", "interactive descr", ""])
        editor = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.prompt_user", prompt_user) as pmu, \
            patch("spacecmd.cryptokey.read_file", read_file) as rfl, \
            patch("spacecmd.cryptokey.editor", editor) as edt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_create(shell, "")

        assert not shell.help_cryptokey_create.called
        assert not shell.client.kickstart.keys.create.called
        assert not read_file.called
        assert not editor.called
        assert prompt_user.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "No contents of the file")

    def test_cryptokey_create_interactive_wrong_key_type(self, shell):
        """
        Test do_cryptokey_create without arguments (interactive, wrong key type).

        :param shell:
        :return:
        """
        shell.help_cryptokey_create = MagicMock()
        shell.client.kickstart.keys.create = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        read_file = MagicMock(return_value="contents")
        prompt_user = MagicMock(side_effect=["x", "interactive descr", "/tmp/file.txt"])
        editor = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.prompt_user", prompt_user) as pmu, \
            patch("spacecmd.cryptokey.read_file", read_file) as rfl, \
            patch("spacecmd.cryptokey.editor", editor) as edt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_create(shell, "")

        assert not shell.help_cryptokey_create.called
        assert not shell.client.kickstart.keys.create.called
        assert not editor.called
        assert read_file.called
        assert prompt_user.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "Invalid key type")

    def test_cryptokey_create_GPG_key(self, shell):
        """
        Test do_cryptokey_create with parameters, calling GPG key type.

        :param shell:
        :return:
        """
        shell.help_cryptokey_create = MagicMock()
        shell.client.kickstart.keys.create = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        read_file = MagicMock(return_value="contents")
        prompt_user = MagicMock(side_effect=[])
        editor = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.prompt_user", prompt_user) as pmu, \
            patch("spacecmd.cryptokey.read_file", read_file) as rfl, \
            patch("spacecmd.cryptokey.editor", editor) as edt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_create(shell, "-t g -d description -f /tmp/file.txt")

        assert not editor.called
        assert not shell.help_cryptokey_create.called
        assert not prompt_user.called
        assert not logger.error.called

        assert shell.client.kickstart.keys.create.called
        assert read_file.called

        for call in shell.client.kickstart.keys.create.call_args_list:
            args, kw = call
            assert args == (shell.session, "description", "GPG", "contents")
            assert not kw

    def test_cryptokey_create_SSL_key(self, shell):
        """
        Test do_cryptokey_create with parameters, calling SSL key type.

        :param shell:
        :return:
        """
        shell.help_cryptokey_create = MagicMock()
        shell.client.kickstart.keys.create = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        read_file = MagicMock(return_value="contents")
        prompt_user = MagicMock(side_effect=[])
        editor = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.prompt_user", prompt_user) as pmu, \
            patch("spacecmd.cryptokey.read_file", read_file) as rfl, \
            patch("spacecmd.cryptokey.editor", editor) as edt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_create(shell, "-t s -d description -f /tmp/file.txt")

        assert not editor.called
        assert not shell.help_cryptokey_create.called
        assert not prompt_user.called
        assert not logger.error.called

        assert shell.client.kickstart.keys.create.called
        assert read_file.called

        for call in shell.client.kickstart.keys.create.call_args_list:
            args, kw = call
            assert args == (shell.session, "description", "SSL", "contents")
            assert not kw

    def test_cryptokey_delete_noargs(self, shell):
        """
        Test do_cryptokey_delete without parameters, so help should be displayed.

        :return:
        """
        shell.help_cryptokey_delete = MagicMock()
        shell.client.kickstart.keys.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        shell.do_cryptokey_list = MagicMock()
        filter_results = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.cryptokey.logging", logger) as lgr, \
            patch("spacecmd.cryptokey.filter_results", filter_results) as frl:
            spacecmd.cryptokey.do_cryptokey_delete(shell, "")

        assert not logger.error.called
        assert not filter_results.called
        assert not shell.client.kickstart.keys.delete.called
        assert not shell.user_confirm.called
        assert not shell.do_cryptokey_list.called
        assert shell.help_cryptokey_delete.called

    def test_cryptokey_delete_no_exist(self, shell):
        """
        Test do_cryptokey_delete with non-existing key.

        :return:
        """
        shell.help_cryptokey_delete = MagicMock()
        shell.client.kickstart.keys.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        shell.do_cryptokey_list = MagicMock(return_value=["one", "two", "three"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.logging", logger) as lgr, \
            patch("spacecmd.cryptokey.print", mprint) as prn:
            spacecmd.cryptokey.do_cryptokey_delete(shell, "foo*")

        assert not shell.client.kickstart.keys.delete.called
        assert not shell.help_cryptokey_delete.called
        assert not shell.user_confirm.called
        assert not mprint.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "No keys matched argument ['foo.*']")

    def test_cryptokey_delete_not_confirmed(self, shell):
        """
        Test do_cryptokey_delete with non-existing key.

        :return:
        """
        shell.help_cryptokey_delete = MagicMock()
        shell.client.kickstart.keys.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=False)
        shell.do_cryptokey_list = MagicMock(return_value=["one", "two", "three"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.logging", logger) as lgr, \
            patch("spacecmd.cryptokey.print", mprint) as prn:
            spacecmd.cryptokey.do_cryptokey_delete(shell, "t*")

        assert not shell.client.kickstart.keys.delete.called
        assert not shell.help_cryptokey_delete.called
        assert not logger.error.called
        assert shell.user_confirm.called
        assert mprint.called

        assert_expect(mprint.call_args_list, 'three\ntwo')

    def test_cryptokey_delete_confirmed_deleted(self, shell):
        """
        Test do_cryptokey_delete with non-existing key.

        :return:
        """
        shell.help_cryptokey_delete = MagicMock()
        shell.client.kickstart.keys.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        shell.do_cryptokey_list = MagicMock(return_value=["one", "two", "three"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.logging", logger) as lgr, \
            patch("spacecmd.cryptokey.print", mprint) as prn:
            spacecmd.cryptokey.do_cryptokey_delete(shell, "t*")

        assert not logger.error.called
        assert not shell.help_cryptokey_delete.called
        assert shell.client.kickstart.keys.delete.called
        assert shell.user_confirm.called
        assert mprint.called

        assert_expect(mprint.call_args_list, 'three\ntwo')
        exp = [
            (shell.session, "two",),
            (shell.session, "three",),
        ]

        for call in shell.client.kickstart.keys.delete.call_args_list:
            args, kw = call
            assert not kw
            assert args == next(iter(exp))
            exp.pop(0)
        assert not exp

    def test_cryptokey_list_no_stdout(self, shell):
        """
        Test do_cryptokey_list no STDOUT.

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.listAllKeys = MagicMock(
            return_value=[
                {"description": "keydescr-1"},
                {"description": "keydescr-2"},
            ]
        )
        mprint = MagicMock()
        with patch("spacecmd.cryptokey.print", mprint) as prn:
            out = spacecmd.cryptokey.do_cryptokey_list(shell, "", doreturn=True)

        assert not mprint.called
        assert bool(out)
        assert shell.client.kickstart.keys.listAllKeys.called
        assert out == ['keydescr-1', 'keydescr-2']

    def test_cryptokey_list_stdout(self, shell):
        """
        Test do_cryptokey_list to STDOUT.

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.listAllKeys = MagicMock(
            return_value=[
                {"description": "keydescr-1"},
                {"description": "keydescr-2"},
            ]
        )
        mprint = MagicMock()
        with patch("spacecmd.cryptokey.print", mprint) as prn:
            out = spacecmd.cryptokey.do_cryptokey_list(shell, "", doreturn=False)

        assert out is None
        assert mprint.called
        assert shell.client.kickstart.keys.listAllKeys.called

        assert_expect(mprint.call_args_list, "keydescr-1\nkeydescr-2")

    def test_cryptokey_details_noargs(self, shell):
        """
        Test do_cryptokey_details with no parameters.

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.getDetails = MagicMock()
        shell.do_cryptokey_list = MagicMock(return_value=[])
        shell.help_cryptokey_details = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.print", mprint) as mpt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_details(shell, "")

        assert not mprint.called
        assert not shell.client.kickstart.keys.getDetails.called
        assert not shell.do_cryptokey_list.called
        assert shell.help_cryptokey_details.called

    def test_cryptokey_details_not_found(self, shell):
        """
        Test do_cryptokey_details key not found.

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.getDetails = MagicMock()
        shell.do_cryptokey_list = MagicMock(return_value=[])
        shell.help_cryptokey_details = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.print", mprint) as mpt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_details(shell, "somekey")

        assert not mprint.called
        assert not shell.client.kickstart.keys.getDetails.called
        assert not shell.help_cryptokey_details.called
        assert shell.do_cryptokey_list.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "No keys matched argument ['somekey']")

    def test_cryptokey_details_listing(self, shell):
        """
        Test do_cryptokey_details key listing

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.getDetails = MagicMock(side_effect=[
            {"description": "first descr", "type": "SSL", "content": "one data"},
            {"description": "second descr", "type": "GPG", "content": "two data"},
        ])
        shell.do_cryptokey_list = MagicMock(return_value=["key-one", "key-two", "three"])
        shell.SEPARATOR = "---"
        shell.help_cryptokey_details = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.print", mprint) as mpt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_details(shell, "key*")

        assert not shell.help_cryptokey_details.called
        assert not logger.error.called
        assert logger.debug.called
        assert mprint.called
        assert shell.client.kickstart.keys.getDetails.called
        assert shell.do_cryptokey_list.called

        exp = [
            'Description: first descr',
            'Type:        SSL', '', 'one data', '---',
            'Description: second descr',
            'Type:        GPG', '', 'two data',
        ]

        for call in mprint.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)
        assert not exp

    def test_cryptokey_details_rpc_error(self, shell):
        """
        Test do_cryptokey_details captures xmlrpc failure.

        :param shell:
        :return:
        """
        shell.client.kickstart.keys.getDetails = MagicMock(
            side_effect=xmlrpclib.Fault(faultCode=42, faultString="Kaboom")
        )
        shell.do_cryptokey_list = MagicMock(return_value=["somekey"])
        shell.help_cryptokey_details = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.cryptokey.print", mprint) as mpt, \
            patch("spacecmd.cryptokey.logging", logger) as lgr:
            spacecmd.cryptokey.do_cryptokey_details(shell, "somekey")

        assert not mprint.called
        assert not shell.help_cryptokey_details.called
        assert not logger.error.called
        assert shell.client.kickstart.keys.getDetails.called
        assert shell.do_cryptokey_list.called
        assert logger.warning.called

        assert_expect(logger.warning.call_args_list, "somekey is not a valid crypto key")
07070100000033000081B40000000000000000000000015DA8415F0000340F000000000000000000000000000000000000002200000000spacecmd/tests/test_custominfo.py# coding: utf-8
"""
Test suite for custominfo source
"""
from mock import MagicMock, patch, mock_open
from spacecmd import custominfo
from helpers import shell
import pytest


class TestSCCusomInfo:
    """
    Test for custominfo API.
    """
    def test_do_custominfo_createkey_no_keyname(self, shell):
        """
        Test do_custominfo_createkey do not break on no key name provided, falling back to interactive mode.
        """
        shell.client.system.custominfo.createKey = MagicMock()
        prompter = MagicMock(side_effect=["", "", Exception("Empty key")])

        with patch("spacecmd.custominfo.prompt_user", prompter):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_createkey(shell, "")

        assert "Empty key" in str(exc)


    def test_do_custominfo_createkey_no_descr(self, shell):
        """
        Test do_custominfo_createkey description gets the name of the key, if not provided.
        """
        shell.client.system.custominfo.createKey = MagicMock()
        prompter = MagicMock(side_effect=["keyname", ""])

        with patch("spacecmd.custominfo.prompt_user", prompter):
            custominfo.do_custominfo_createkey(shell, "")

        assert shell.client.system.custominfo.createKey.called
        session, keyname, descr = shell.client.system.custominfo.createKey.call_args_list[0][0]
        assert shell.session == session
        assert keyname == descr

    def test_do_custominfo_createkey_descr_interactive(self, shell):
        """
        Test do_custominfo_createkey description gets the name of the key from interactive prompt.
        """
        shell.client.system.custominfo.createKey = MagicMock()
        prompter = MagicMock(side_effect=["keyname", "keydescr"])

        with patch("spacecmd.custominfo.prompt_user", prompter):
            custominfo.do_custominfo_createkey(shell, "")

        assert shell.client.system.custominfo.createKey.called
        session, keyname, descr = shell.client.system.custominfo.createKey.call_args_list[0][0]
        assert shell.session == session
        assert keyname != descr
        assert keyname == "keyname"
        assert descr == "keydescr"

    def test_do_custominfo_createkey_descr_args(self, shell):
        """
        Test do_custominfo_createkey description gets the name of the key from the args.
        """
        shell.client.system.custominfo.createKey = MagicMock()
        prompter = MagicMock(side_effect=Exception("Kaboom"))

        custominfo.do_custominfo_createkey(shell, "keyname keydescr")

        assert shell.client.system.custominfo.createKey.called
        session, keyname, descr = shell.client.system.custominfo.createKey.call_args_list[0][0]
        assert shell.session == session
        assert keyname != descr
        assert keyname == "keyname"
        assert descr == "keydescr"

    def test_do_custominfo_deletekey_noargs(self, shell):
        """
        Test do_custominfo_deletekey shows help on no args.
        """
        errmsg = "No arguments passed"
        shell.client.system.custominfo.deleteKey = MagicMock()
        shell.do_custominfo_listkeys = MagicMock()
        shell.help_custominfo_deletekey = MagicMock(side_effect=Exception(errmsg))
        shell.user_confirm = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_deletekey(shell, "")

        assert errmsg in str(exc)

    @patch("spacecmd.custominfo.print", MagicMock())
    def test_do_custominfo_deletekey_args(self, shell):
        """
        Test do_custominfo_deletekey calls deleteKey API function.
        """
        keylist = ["some_key", "some_other_key", "this_key_stays"]
        shell.client.system.custominfo.deleteKey = MagicMock()
        shell.do_custominfo_listkeys = MagicMock(return_value=keylist)
        shell.help_custominfo_deletekey = MagicMock(side_effect=Exception("Kaboom"))
        shell.user_confirm = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            custominfo.do_custominfo_deletekey(shell, "some*")

        assert shell.client.system.custominfo.deleteKey.called
        for call in shell.client.system.custominfo.deleteKey.call_args_list:
            session, keyname = call[0]
            assert shell.session == session
            assert keyname in keylist
            keylist.pop(keylist.index(keyname))
        assert len(keylist) == 1
        assert "this_key_stays" in keylist

    def test_do_custominfo_listkeys_stdout(self, shell):
        """
        Test do_custominfo_listkeys calls lists all keys calling listAllKeys API function to STDOUT.
        """
        keylist=[
            {"label": "some_key"},
            {"label": "some_other_key"},
            {"label": "this_key_stays"},
        ]
        shell.client.system.custominfo.listAllKeys = MagicMock(return_value=keylist)
        mprint = MagicMock()
        with patch("spacecmd.custominfo.print", mprint):
            ret = custominfo.do_custominfo_listkeys(shell, "")

        assert ret is None
        assert mprint.called

    def test_do_custominfo_listkeys_as_data(self, shell):
        """
        Test do_custominfo_listkeys calls lists all keys calling listAllKeys API function as data.
        """
        keylist=[
            {"label": "some_key"},
            {"label": "some_other_key"},
            {"label": "this_key_stays"},
        ]
        shell.client.system.custominfo.listAllKeys = MagicMock(return_value=keylist)
        mprint = MagicMock()
        with patch("spacecmd.custominfo.print", mprint):
            ret = custominfo.do_custominfo_listkeys(shell, "", doreturn=True)

        assert not mprint.called
        assert isinstance(ret, list)
        for key in keylist:
            assert key["label"] in ret

    def test_do_custominfo_details_noarg(self, shell):
        """
        Test do_custominfo_details shows help when no arguments has been passed.
        """
        keylist=["some_key", "some_other_key", "this_key_stays"]
        shell.help_custominfo_details = MagicMock(side_effect=Exception("Help info"))
        shell.client.system.custominfo.listAllKeys = MagicMock()
        shell.do_custominfo_listkeys = MagicMock(return_value=keylist)
        logger = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_details(shell, "")

        assert "Help info" in str(exc)
        assert not logger.debug.called
        assert not logger.error.called
        assert not shell.client.system.custominfo.listAllKeys.called

    def test_do_custominfo_details_no_key(self, shell):
        """
        Test do_custominfo_details shows error to the log if key name doesn't match.
        """
        shell.client.system.custominfo.listAllKeys = MagicMock()
        shell.do_custominfo_listkeys = MagicMock(return_value=["key_one", "key_two"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with patch("spacecmd.custominfo.print", mprint):
                custominfo.do_custominfo_details(shell, "keyname")

        assert not shell.client.system.custominfo.listAllKeys.called
        assert logger.debug.call_args_list[0][0][0] == "customkey_details called with args: 'keyname', keys: ''."
        assert logger.error.call_args_list[0][0][0] == "No keys matched argument 'keyname'."

    def test_do_custominfo_details_keydetails_notfound(self, shell):
        """
        Test do_custominfo_details nothing happens if keydetails missing.
        """
        shell.client.system.custominfo.listAllKeys = MagicMock()
        shell.do_custominfo_listkeys = MagicMock(return_value=["key_one", "key_two"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with patch("spacecmd.custominfo.print", mprint):
                custominfo.do_custominfo_details(shell, "key*")

        assert shell.client.system.custominfo.listAllKeys.called
        assert not mprint.called

    def test_do_custominfo_details_keydetails_na(self, shell):
        """
        Test do_custominfo_details prints key details not available in format.
        """
        shell.SEPARATOR = "***"
        shell.client.system.custominfo.listAllKeys = MagicMock(
            return_value=[
                {"label": "key_one"},
                {"label": "key_two"},
            ]
        )
        shell.do_custominfo_listkeys = MagicMock(return_value=["key_one", "key_two"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with patch("spacecmd.custominfo.print", mprint):
                custominfo.do_custominfo_details(shell, "key*")

        expectations = [
            'Label:        key_one',
            'Description:  N/A',
            'Modified:     N/A',
            'System Count: 0',
            '***',
            'Label:        key_two',
            'Description:  N/A',
            'Modified:     N/A',
            'System Count: 0'
        ]

        assert shell.client.system.custominfo.listAllKeys.called
        assert mprint.called
        for idx, call in enumerate(mprint.call_args_list):
            assert call[0][0] == expectations[idx]

    def test_do_custominfo_details_keydetails(self, shell):
        """
        Test do_custominfo_details prints key details not available in format.
        """
        shell.SEPARATOR = "***"
        shell.client.system.custominfo.listAllKeys = MagicMock(
            return_value=[
                {"label": "key_one", "description": "descr one", "last_modified": "123", "system_count": 1},
                {"label": "key_two", "description": "descr two", "last_modified": "234", "system_count": 2},
            ]
        )
        shell.do_custominfo_listkeys = MagicMock(return_value=["key_one", "key_two"])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.custominfo.logging", logger):
            with patch("spacecmd.custominfo.print", mprint):
                custominfo.do_custominfo_details(shell, "key*")

        expectations = [
            'Label:        key_one',
            'Description:  descr one',
            'Modified:     123',
            'System Count: 1',
            '***',
            'Label:        key_two',
            'Description:  descr two',
            'Modified:     234',
            'System Count: 2'
        ]

        assert shell.client.system.custominfo.listAllKeys.called
        assert mprint.called
        for idx, call in enumerate(mprint.call_args_list):
            assert call[0][0] == expectations[idx]

    def test_custominfo_updatekey_noarg_name(self, shell):
        """
        Test do_custominfo_updatekey with no arguments falls to the interactive prompt.
        """
        shell.client.system.custominfo.updateKey = MagicMock()
        prompt = MagicMock(side_effect=Exception("interactive mode"))
        with patch("spacecmd.custominfo.prompt_user", prompt):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_updatekey(shell, "")

        assert "interactive mode" in str(exc)

    def test_custominfo_updatekey_noarg_descr(self, shell):
        """
        Test do_custominfo_updatekey with no arguments falls to the interactive prompt.
        """
        shell.client.system.custominfo.updateKey = MagicMock()
        prompt = MagicMock(side_effect=["keyname", Exception("interactive mode for descr")])
        with patch("spacecmd.custominfo.prompt_user", prompt):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_updatekey(shell, "")

        assert "interactive mode for descr" in str(exc)

    def test_custominfo_updatekey_keyonly_arg(self, shell):
        """
        Test do_custominfo_updatekey description is taken interactively.
        """
        shell.client.system.custominfo.updateKey = MagicMock()
        prompt = MagicMock(side_effect=[Exception("interactive mode for descr")])
        with patch("spacecmd.custominfo.prompt_user", prompt):
            with pytest.raises(Exception) as exc:
                custominfo.do_custominfo_updatekey(shell, "keyname")

        assert "interactive mode for descr" in str(exc)

    def test_custominfo_updatekey_all_args(self, shell):
        """
        Test do_custominfo_updatekey description is taken by arguments, interactive mode is not initiated.
        """
        shell.client.system.custominfo.updateKey = MagicMock()
        prompt = MagicMock(side_effect=[Exception("interactive mode for descr")])
        with patch("spacecmd.custominfo.prompt_user", prompt):
            custominfo.do_custominfo_updatekey(shell, "keyname 'some key description here'")

        assert shell.client.system.custominfo.updateKey.called
        session, keyname, description = shell.client.system.custominfo.updateKey.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "keyname"
        assert description == "some key description here"
07070100000034000081B40000000000000000000000015DA8415F000054E7000000000000000000000000000000000000002400000000spacecmd/tests/test_distribution.py# coding: utf-8
"""
Test distribution
"""

from unittest.mock import MagicMock, patch
from helpers import shell, assert_expect
import pytest
import spacecmd.distribution


class TestSCDistribution:
    """
    Test suite for distribution commands of the spacecmd.
    """
    def test_distribution_create_no_args(self, shell):
        """
        Test do_distribution_create with no args.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock(side_effect=[
            "name", "/path/tree", "base-channel", "image"
        ])
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(shell, "")

        assert mprint.called
        assert prompt.called
        assert shell.client.kickstart.tree.listInstallTypes.called
        assert shell.client.kickstart.tree.create.called
        assert not shell.client.kickstart.tree.update.called

        # Check STDOUT consistency
        exp = [
            '', 'Base Channels',
            '-------------', 'base-channel', '', '',
            'Install Types', '-------------', 'image', ''
        ]
        for call in mprint.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)
        assert not exp

        for call in shell.client.kickstart.tree.create.call_args_list:
            args, kw = call
            assert args == (shell.session, "name", "/path/tree", "base-channel", "image")
            assert not kw

        assert_expect(shell.client.kickstart.tree.listInstallTypes.call_args_list,
                      shell.session)

    def test_distribution_create_no_args_update_mode(self, shell):
        """
        Test do_distribution_create with no args with update mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock(side_effect=[
            "name", "/path/tree", "base-channel", "image"
        ])
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(shell, "", update=True)

        assert not mprint.called
        assert not prompt.called
        assert not shell.client.kickstart.tree.listInstallTypes.called
        assert not shell.client.kickstart.tree.create.called
        assert not shell.client.kickstart.tree.update.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "The name of the distribution is required")

    def test_distribution_create_args_ds_update_mode(self, shell):
        """
        Test do_distribution_create with distribution name in update mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(shell, "-n myname", update=True)

        assert not mprint.called
        assert not prompt.called
        assert not shell.client.kickstart.tree.listInstallTypes.called
        assert not shell.client.kickstart.tree.create.called
        assert not shell.client.kickstart.tree.update.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "A path is required")

    def test_distribution_create_args_dspt_update_mode(self, shell):
        """
        Test do_distribution_create with distribution name and path in update mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(
                shell, "-n myname -p /path/tree", update=True)

        assert not mprint.called
        assert not prompt.called
        assert not shell.client.kickstart.tree.listInstallTypes.called
        assert not shell.client.kickstart.tree.create.called
        assert not shell.client.kickstart.tree.update.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "A base channel is required")

    def test_distribution_create_args_dsptbc_update_mode(self, shell):
        """
        Test do_distribution_create with distribution name, path and base channel
        in update mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(
                shell, "-n myname -p /path/tree -b base-channel", update=True)

        assert not mprint.called
        assert not prompt.called
        assert not shell.client.kickstart.tree.listInstallTypes.called
        assert not shell.client.kickstart.tree.create.called
        assert not shell.client.kickstart.tree.update.called
        assert logger.error.called

        assert_expect(logger.error.call_args_list, "An install type is required")

    def test_distribution_create_args_dsptbcit_update_mode(self, shell):
        """
        Test do_distribution_create with distribution name, path, base channel
        and install type in update mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.tree.listInstallTypes = MagicMock(return_value=[
            {"label": "image"},
        ])
        shell.client.kickstart.tree.update = MagicMock()
        shell.client.kickstart.tree.create = MagicMock()
        shell.list_base_channels = MagicMock(return_value=["base-channel"])

        mprint = MagicMock()
        prompt = MagicMock()
        logger = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
            patch("spacecmd.distribution.prompt_user", prompt) as prmt, \
            patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_create(
                shell, "-n myname -p /path/tree -b base-channel -t image", update=True)

        assert not mprint.called
        assert not prompt.called
        assert not shell.client.kickstart.tree.create.called
        assert not logger.error.called
        assert not shell.client.kickstart.tree.listInstallTypes.called
        assert shell.client.kickstart.tree.update.called

        for call in shell.client.kickstart.tree.update.call_args_list:
            args, kw = call
            assert args == (shell.session, "myname", "/path/tree", "base-channel", "image")
            assert not kw

    def test_distribution_list_noarg_noret(self, shell):
        """
        Test do_distribution_list without argumnets, no return option.

        :param shell:
        :return:
        """
        shell.client.kickstart.listAutoinstallableChannels = MagicMock(return_value=[
            {"label": "channel-name"},
        ])
        shell.client.kickstart.tree.list = MagicMock(return_value=[
            {"label": "some-channel"},
            {"label": "some-other-channel"},
        ])
        mprint = MagicMock()
        with patch("spacecmd.distribution.print", mprint) as prn:
            out = spacecmd.distribution.do_distribution_list(shell, "")

        assert out is None
        assert mprint.called
        assert_expect(mprint.call_args_list, "some-channel\nsome-other-channel")

    def test_distribution_list_noarg_ret(self, shell):
        """
        Test do_distribution_list without argumnets, return data mode.

        :param shell:
        :return:
        """
        shell.client.kickstart.listAutoinstallableChannels = MagicMock(return_value=[
            {"label": "channel-name"},
        ])
        shell.client.kickstart.tree.list = MagicMock(return_value=[
            {"label": "some-channel"},
            {"label": "some-other-channel"},
        ])
        mprint = MagicMock()
        with patch("spacecmd.distribution.print", mprint) as prn:
            out = spacecmd.distribution.do_distribution_list(shell, "", doreturn=True)

        assert out is not None
        assert type(out) == list
        assert not mprint.called
        assert out == ['some-channel', 'some-other-channel']

    def test_distribution_delete_noargs(self, shell):
        """
        Test do_distribution_delete with no arguments.

        :param shell:
        :return:
        """
        shell.do_distribution_list = MagicMock()
        shell.help_distribution_delete = MagicMock()
        shell.client.kickstart.tree.delete = MagicMock()
        shell.user_confirm = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_delete(shell, "")

        assert not logger.debug.called
        assert not logger.error.called
        assert not mprint.called
        assert not shell.do_distribution_list.called
        assert not shell.client.kickstart.tree.delete.called
        assert not shell.user_confirm.called
        assert shell.help_distribution_delete.called

    def test_distribution_delete_args_no_match(self, shell):
        """
        Test do_distribution_delete with wrong arguments.

        :param shell:
        :return:
        """
        shell.do_distribution_list = MagicMock(return_value=["bar"])
        shell.help_distribution_delete = MagicMock()
        shell.client.kickstart.tree.delete = MagicMock()
        shell.user_confirm = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_delete(shell, "foo*")

        assert logger.debug.called
        assert logger.error.called
        assert not mprint.called
        assert not shell.client.kickstart.tree.delete.called
        assert not shell.user_confirm.called
        assert not shell.help_distribution_delete.called

        assert_expect(logger.debug.call_args_list,
                      "distribution_delete called with args ['foo.*'], dists=[]")
        assert_expect(logger.error.call_args_list,
                      "No distributions matched argument ['foo.*']")

    def test_distribution_delete_args_match_no_confirm(self, shell):
        """
        Test do_distribution_delete with correct arguments, not confirmed to delete.

        :param shell:
        :return:
        """
        shell.do_distribution_list = MagicMock(return_value=["bar"])
        shell.help_distribution_delete = MagicMock()
        shell.client.kickstart.tree.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=False)
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_delete(shell, "b*")

        assert not logger.error.called
        assert not shell.client.kickstart.tree.delete.called
        assert not shell.help_distribution_delete.called
        assert logger.debug.called
        assert mprint.called
        assert shell.user_confirm.called

        assert_expect(logger.debug.call_args_list,
                      "distribution_delete called with args ['b.*'], dists=['bar']")
        assert_expect(shell.user_confirm.call_args_list,
                      "Delete distribution tree(s) [y/N]:")
        assert_expect(mprint.call_args_list, "bar")

    def test_distribution_delete_args_match_confirm(self, shell):
        """
        Test do_distribution_delete with correct arguments, confirmed to delete.

        :param shell:
        :return:
        """
        shell.do_distribution_list = MagicMock(return_value=["bar"])
        shell.help_distribution_delete = MagicMock()
        shell.client.kickstart.tree.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_delete(shell, "b*")

        assert not logger.error.called
        assert not shell.help_distribution_delete.called
        assert shell.client.kickstart.tree.delete.called
        assert logger.debug.called
        assert mprint.called
        assert shell.user_confirm.called

        assert_expect(logger.debug.call_args_list,
                      "distribution_delete called with args ['b.*'], dists=['bar']")
        assert_expect(shell.user_confirm.call_args_list,
                      "Delete distribution tree(s) [y/N]:")
        assert_expect(mprint.call_args_list, "bar")

        for call in shell.client.kickstart.tree.delete.call_args_list:
            args, kw = call
            assert args == (shell.session, "bar",)

    def test_distribution_details_noargs(self, shell):
        """
        Test do_distribution_details with no arguments.

        :param shell:
        :return:
        """
        shell.help_distribution_details = MagicMock()
        shell.client.kickstart.tree.getDetails = MagicMock()
        shell.client.channel.software.getDetails = MagicMock()
        shell.do_distribution_list = MagicMock()
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_details(shell, "")

        assert not logger.error.called
        assert not logger.debug.called
        assert not shell.client.kickstart.tree.getDetails.called
        assert not shell.client.channel.software.getDetails.called
        assert not shell.do_distribution_list.called
        assert shell.help_distribution_details.called

    def test_distribution_details_no_dists(self, shell):
        """
        Test do_distribution_details with no distributions found.

        :param shell:
        :return:
        """
        shell.help_distribution_details = MagicMock()
        shell.client.kickstart.tree.getDetails = MagicMock()
        shell.client.channel.software.getDetails = MagicMock()
        shell.do_distribution_list = MagicMock(return_value=[])
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_details(shell, "test*")

        assert not shell.client.kickstart.tree.getDetails.called
        assert not shell.client.channel.software.getDetails.called
        assert not shell.help_distribution_details.called
        assert not mprint.called
        assert logger.debug.called
        assert logger.error.called
        assert shell.do_distribution_list.called

        assert_expect(logger.debug.call_args_list,
                      "distribution_details called with args ['test.*'], dists=[]")
        assert_expect(logger.error.call_args_list,
                      "No distributions matched argument ['test.*']")

    def test_distribution_details_list(self, shell):
        """
        Test do_distribution_details lister.

        :param shell:
        :return:
        """
        shell.help_distribution_details = MagicMock()
        shell.client.kickstart.tree.getDetails = MagicMock(side_effect=[
            {"channel_id": "ch-id-1", "label": "dist-1", "abs_path": "/tmp/d1"},
            {"channel_id": "ch-id-2", "label": "dist-2", "abs_path": "/tmp/d2"},
        ])
        shell.client.channel.software.getDetails = MagicMock(side_effect=[
            {"label": "channel-one"},
            {"label": "channel-two"},
        ])
        shell.do_distribution_list = MagicMock(return_value=["dist-1", "dist-2"])
        shell.SEPARATOR = "---"
        logger = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.distribution.print", mprint) as prn, \
                patch("spacecmd.distribution.logging", logger) as lgr:
            spacecmd.distribution.do_distribution_details(shell, "dist*")

        assert not shell.help_distribution_details.called
        assert not logger.error.called
        assert shell.client.kickstart.tree.getDetails.called
        assert shell.client.channel.software.getDetails.called
        assert logger.debug.called
        assert shell.do_distribution_list.called
        assert mprint.called

        exp = [
            'Name:    dist-1',
            'Path:    /tmp/d1',
            'Channel: channel-one',
            '---',
            'Name:    dist-2',
            'Path:    /tmp/d2',
            'Channel: channel-two'
        ]
        for call in mprint.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)
        assert not exp

    def test_distribution_rename_noargs(self, shell):
        """
        Test do_distribution_rename without arguments.

        :param shell:
        :return:
        """
        for args in ["", "foo"]:
            shell.help_distribution_rename = MagicMock()
            shell.client.kickstart.tree.rename = MagicMock()

            spacecmd.distribution.do_distribution_rename(shell, "")

            assert not shell.client.kickstart.tree.rename.called
            assert shell.help_distribution_rename.called

    def test_distribution_rename(self, shell):
        """
        Test do_distribution_rename.

        :param shell:
        :return:
        """
        shell.help_distribution_rename = MagicMock()
        shell.client.kickstart.tree.rename = MagicMock()

        spacecmd.distribution.do_distribution_rename(shell, "source destination")

        assert not shell.help_distribution_rename.called
        assert shell.client.kickstart.tree.rename.called

        for call in shell.client.kickstart.tree.rename.call_args_list:
            args, kw = call
            assert args == (shell.session, "source", "destination")

    def test_distribution_update_noargs(self, shell):
        """
        Test do_distribution_update without arguments.

        :param shell:
        :return:
        """

        shell.help_distribution_update = MagicMock()
        shell.do_distribution_create = MagicMock()

        spacecmd.distribution.do_distribution_update(shell, "")

        assert not shell.do_distribution_create.called
        assert shell.help_distribution_update.called

    def test_distribution_update(self, shell):
        """
        Test do_distribution_update.

        :param shell:
        :return:
        """

        shell.help_distribution_update = MagicMock()
        shell.do_distribution_create = MagicMock()

        spacecmd.distribution.do_distribution_update(shell, "my-distro")

        assert not shell.help_distribution_update.called
        assert shell.do_distribution_create.called

        for call in shell.do_distribution_create.call_args_list:
            args, kw = call
            assert args == ("my-distro", )
            assert kw == {"update": True}
07070100000035000081B40000000000000000000000015DA8415F000013F9000000000000000000000000000000000000002800000000spacecmd/tests/test_filepreservation.py# coding: utf-8
"""
Test suite for spacecmd.filepreservation
"""
from unittest.mock import MagicMock, patch, mock_open
import spacecmd.filepreservation
from helpers import shell


class TestSCFilePreservation:
    """
    Test class for testing spacecmd file preservation.
    """
    def test_do_filepreservation_list_noreturn(self, shell):
        """
        Test do_filepreservation_list return to the STDOUT
        """
        shell.client.kickstart.filepreservation.listAllFilePreservations = MagicMock(
            return_value=[
                {"name": "list_one"},
                {"name": "list_two"},
                {"name": "list_three"},
            ]
        )

        mprint = MagicMock()
        with patch("spacecmd.filepreservation.print", mprint):
            ret = spacecmd.filepreservation.do_filepreservation_list(shell, "", doreturn=False)
        assert ret is None
        assert mprint.call_args_list[0][0][0] == "list_one\nlist_three\nlist_two"

    def test_do_filepreservation_list_return(self, shell):
        """
        Test do_filepreservation_list return data.
        """
        shell.client.kickstart.filepreservation.listAllFilePreservations = MagicMock(
            return_value=[
                {"name": "list_one"},
                {"name": "list_two"},
                {"name": "list_three"},
            ]
        )

        mprint = MagicMock()
        with patch("spacecmd.filepreservation.print", mprint):
            ret = spacecmd.filepreservation.do_filepreservation_list(shell, "", doreturn=True)
        assert not mprint.called
        assert ret == ['list_one', 'list_two', 'list_three']

    def test_do_filepreservation_create_noargs_prompted_name(self, shell):
        """
        Test do_filepreservation_create no args passed so prompt appears.
        """
        shell.client.kickstart.filepreservation.create = MagicMock()

        mprint = MagicMock()
        prmt = MagicMock(side_effect=["prompted_name", "file_one", "file_two", ""])
        with patch("spacecmd.filepreservation.prompt_user", prmt) as pmt, \
             patch("spacecmd.filepreservation.print", mprint) as mpt:
            spacecmd.filepreservation.do_filepreservation_create(shell, "")

        expectations = [
            'File List', '---------', '', '',
            'File List', '---------', 'file_one', '',
            'File List', '---------', 'file_one\nfile_two', '', '',
            'File List', '---------', 'file_one\nfile_two'
        ]
        for idx, call in enumerate(mprint.call_args_list):
            assert call[0][0] == expectations[idx]

    def test_do_filepreservation_delete_noargs(self, shell):
        """
        Test do_filepreservation_delete no args.
        """
        shell.help_filepreservation_delete = MagicMock()
        shell.client.kickstart.filepreservation.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        spacecmd.filepreservation.do_filepreservation_delete(shell, "")

        assert shell.help_filepreservation_delete.called
        assert not shell.client.kickstart.filepreservation.delete.called
        assert not shell.user_confirm.called

    def test_do_filepreservation_delete_args(self, shell):
        """
        Test do_filepreservation_delete key name passed.
        """
        shell.help_filepreservation_delete = MagicMock()
        shell.client.kickstart.filepreservation.delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        spacecmd.filepreservation.do_filepreservation_delete(shell, "some_key")

        assert not shell.help_filepreservation_delete.called
        assert shell.client.kickstart.filepreservation.delete.called
        assert shell.user_confirm.called

        session, keyname = shell.client.kickstart.filepreservation.delete.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "some_key"

    def test_do_filepreservation_details_noargs(self, shell):
        """
        Test do_filepreservation_details no args.
        """
        shell.help_filepreservation_details = MagicMock()
        shell.client.kickstart.filepreservation.getDetails = MagicMock()

        spacecmd.filepreservation.do_filepreservation_details(shell, "")

        assert shell.help_filepreservation_details.called
        assert not shell.client.kickstart.filepreservation.details.called

    @patch("spacecmd.filepreservation.print", MagicMock())
    def test_do_filepreservation_details_args(self, shell):
        """
        Test do_filepreservation_details key passed.
        """
        shell.help_filepreservation_details = MagicMock()
        shell.client.kickstart.filepreservation.getDetails = MagicMock()

        spacecmd.filepreservation.do_filepreservation_details(shell, "somekey")

        assert not shell.help_filepreservation_details.called
        assert shell.client.kickstart.filepreservation.getDetails.called

        session, keyname = shell.client.kickstart.filepreservation.getDetails.call_args_list[0][0]
        assert shell.session == session
        assert keyname == "somekey"
07070100000036000081B40000000000000000000000015DA8415F000038F0000000000000000000000000000000000000001C00000000spacecmd/tests/test_scap.py# coding: utf-8
"""
Test suite for Scap commands at spacecmd.
"""

from unittest.mock import MagicMock, patch
from helpers import shell, assert_expect
import pytest
import spacecmd.scap


class TestScap:
    """
    Test suite for scap.
    """

    def test_scap_listxccdfscans_noarg(self, shell):
        """
        Test calling scap listxccdfscans without arguments.

        :param shell:
        :return:
        """
        shell.help_scap_listxccdfscans = MagicMock()
        shell.ssm = MagicMock()
        shell.expand_systems = MagicMock()
        shell.get_system_id = MagicMock()
        shell.client.system.scap.listXccdfScans = MagicMock()

        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_listxccdfscans(shell, "")

        assert shell.help_scap_listxccdfscans.called
        assert not shell.ssm.keys.called
        assert not shell.expand_systems.called
        assert not shell.get_system_id.called
        assert not shell.client.system.scap.listXccdfScans.called
        assert not mprint.called

    def test_scap_listxccdfscans_ssm_arg(self, shell):
        """
        Test calling scap listxccdfscans with ssm argument.
        No systems has been scanned.

        :param shell:
        :return:
        """
        shell.help_scap_listxccdfscans = MagicMock()
        shell.ssm = MagicMock()
        shell.ssm.keys = MagicMock(return_value=[])
        shell.expand_systems = MagicMock()
        shell.get_system_id = MagicMock()
        shell.client.system.scap.listXccdfScans = MagicMock()

        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_listxccdfscans(shell, "ssm")

        assert not shell.help_scap_listxccdfscans.called
        assert shell.ssm.keys.called
        assert not shell.expand_systems.called
        assert not shell.get_system_id.called
        assert not shell.client.system.scap.listXccdfScans.called
        assert not mprint.called

    def test_scap_listxccdfscans_system_arg(self, shell):
        """
        Test calling scap listxccdfscans with a system name argument.

        :param shell:
        :return:
        """
        shell.help_scap_listxccdfscans = MagicMock()
        shell.SEPARATOR = "---"
        shell.ssm = MagicMock()
        shell.ssm.keys = MagicMock(return_value=[])
        shell.expand_systems = MagicMock(return_value=["chair-1", "table-2"])
        shell.get_system_id = MagicMock(side_effect=["001", "002"])
        shell.client.system.scap.listXccdfScans = MagicMock(side_effect=[
            [
                {"xid": 1, "profile": "001", "path": "/etc/first", "completed": "true"},
                {"xid": 2, "profile": "001", "path": "/etc/second", "completed": "false"},
            ],
            [
                {"xid": 3, "profile": "002", "path": "/opt/etc/third", "completed": "false"},
                {"xid": 4, "profile": "002", "path": "/opt/etc/fourth", "completed": "true"},
            ],
        ])

        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_listxccdfscans(shell, "chair table")

        assert not shell.help_scap_listxccdfscans.called
        assert not shell.ssm.keys.called
        assert shell.expand_systems.called
        assert shell.get_system_id.called
        assert shell.client.system.scap.listXccdfScans.called
        assert mprint.called

        expectations = [
            'System: chair-1', '',
            'XID: 1 Profile: 001 Path: (/etc/first) Completed: true',
            'XID: 2 Profile: 001 Path: (/etc/second) Completed: false',
            shell.SEPARATOR,
            'System: table-2',
            '',
            'XID: 3 Profile: 002 Path: (/opt/etc/third) Completed: false',
            'XID: 4 Profile: 002 Path: (/opt/etc/fourth) Completed: true',
        ]
        assert_expect(mprint.call_args_list, *expectations)

    def test_scap_getxccdfscanruleresults_noargs(self, shell):
        """
        Test getxccdfscanruleresults without args

        :param shell:
        :return:
        """
        shell.help_scap_getxccdfscanruleresults = MagicMock()
        shell.client.system.scap.getXccdfScanRuleResults = MagicMock()
        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_getxccdfscanruleresults(shell, "")

        assert shell.help_scap_getxccdfscanruleresults.called
        assert not shell.client.system.scap.getXccdfScanRuleResults.called
        assert not mprint.called

    def test_scap_getxccdfscanruleresults_xids_no_rules(self, shell):
        """
        Test getxccdfscanruleresults with XIDs but no rules

        :param shell:
        :return:
        """
        shell.help_scap_getxccdfscanruleresults = MagicMock()
        shell.SEPARATOR = "---"
        shell.client.system.scap.getXccdfScanRuleResults = MagicMock()
        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_getxccdfscanruleresults(shell, "1 2 3")

        assert not shell.help_scap_getxccdfscanruleresults.called
        assert shell.client.system.scap.getXccdfScanRuleResults.called
        assert mprint.called

        expectations = [
            'XID: 1', '', '---', 'XID: 2',
            '', '---', 'XID: 3', '',
        ]
        assert_expect(mprint.call_args_list, *expectations)

    def test_scap_getxccdfscanruleresults_xids_with_rules(self, shell):
        """
        Test getxccdfscanruleresults with XIDs with rules

        :param shell:
        :return:
        """
        shell.help_scap_getxccdfscanruleresults = MagicMock()
        shell.SEPARATOR = "---"
        shell.client.system.scap.getXccdfScanRuleResults = MagicMock(side_effect=[
            [
                {"idref": "001A", "result": "result placeholder - 1", "idents": "idents placeholder - 1"},
                {"idref": "001B", "result": "result placeholder - 2", "idents": "idents placeholder - 2"},
            ],
            [
                {"idref": "002A", "result": "result placeholder - 1", "idents": "idents placeholder - 1"},
                {"idref": "002B", "result": "result placeholder - 2", "idents": "idents placeholder - 2"},
            ],
            [
                {"idref": "003A", "result": "result placeholder - 1", "idents": "idents placeholder - 1"},
                {"idref": "003B", "result": "result placeholder - 2", "idents": "idents placeholder - 2"},
            ]
        ])
        mprint = MagicMock()
        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_getxccdfscanruleresults(shell, "1 2 3")

        assert not shell.help_scap_getxccdfscanruleresults.called
        assert shell.client.system.scap.getXccdfScanRuleResults.called
        assert mprint.called

        expectations = [
            'XID: 1', '',
            'IDref: 001A Result: result placeholder - 1 Idents: (idents placeholder - 1)',
            'IDref: 001B Result: result placeholder - 2 Idents: (idents placeholder - 2)',
            '---', 'XID: 2', '',
            'IDref: 002A Result: result placeholder - 1 Idents: (idents placeholder - 1)',
            'IDref: 002B Result: result placeholder - 2 Idents: (idents placeholder - 2)',
            '---', 'XID: 3', '',
            'IDref: 003A Result: result placeholder - 1 Idents: (idents placeholder - 1)',
            'IDref: 003B Result: result placeholder - 2 Idents: (idents placeholder - 2)',
        ]
        assert_expect(mprint.call_args_list, *expectations)

    def test_scap_getxccdfscandetails_no_args(self, shell):
        """
        Test getxccdfscandetails with no args.

        :param shell:
        :return:
        """
        shell.help_scap_getxccdfscandetails = MagicMock()
        shell.SEPARATOR = "---"
        shell.client.system.scap.getXccdfScanDetails = MagicMock()
        mprint = MagicMock()

        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_getxccdfscandetails(shell, "")

        assert shell.help_scap_getxccdfscandetails.called
        assert not shell.client.system.scap.getXccdfScanDetails.called
        assert not mprint.called

    def test_scap_getxccdfscandetails_xids(self, shell):
        """
        Test getxccdfscandetails with XID args.

        :param shell:
        :return:
        """
        shell.help_scap_getxccdfscandetails = MagicMock()
        shell.SEPARATOR = "---"
        shell.client.system.scap.getXccdfScanDetails = MagicMock(side_effect=[
            {
                "xid": 1, "sid": "0001", "action_id": "a1", "path": "p1",
                "oscap_parameters": "op1", "test_result": "tr1", "benchmark": "b1",
                "benchmark_version": "bv1", "profile": "p1", "profile_title": "pt1",
                "start_time": "st1", "end_time": "et1", "errors": "e1",
            },
            {
                "xid": 2, "sid": "0002", "action_id": "a2", "path": "p2",
                "oscap_parameters": "op2", "test_result": "tr2", "benchmark": "b2",
                "benchmark_version": "bv2", "profile": "p2", "profile_title": "pt2",
                "start_time": "st2", "end_time": "et2", "errors": "e2",
            },
        ])
        mprint = MagicMock()

        with patch("spacecmd.scap.print", mprint):
            spacecmd.scap.do_scap_getxccdfscandetails(shell, "1 2")

        assert not shell.help_scap_getxccdfscandetails.called
        assert shell.client.system.scap.getXccdfScanDetails.called
        assert mprint.called

        expectations = [
            (shell.session, 1),
            (shell.session, 2)
        ]
        for call in shell.client.system.scap.getXccdfScanDetails.call_args_list:
            arg, kw = call
            assert kw == {}
            assert arg == next(iter(expectations))
            expectations.pop(0)

        expectations = [
            ('XID: 1',), ('',),
            ('XID:', 1, 'SID:', '0001', 'Action_ID:', 'a1', 'Path:',
             'p1', 'OSCAP_Parameters:', 'op1', 'Test_Result:', 'tr1',
             'Benchmark:', 'b1', 'Benchmark_Version:', 'bv1',
             'Profile:', 'p1', 'Profile_Title:', 'pt1', 'Start_Time:',
             'st1', 'End_Time:', 'et1', 'Errors:', 'e1'),
            ('---',),
            ('XID: 2',), ('',),
            ('XID:', 2, 'SID:', '0002', 'Action_ID:', 'a2', 'Path:',
             'p2', 'OSCAP_Parameters:', 'op2', 'Test_Result:', 'tr2',
             'Benchmark:', 'b2', 'Benchmark_Version:', 'bv2',
             'Profile:', 'p2', 'Profile_Title:', 'pt2', 'Start_Time:',
             'st2', 'End_Time:', 'et2', 'Errors:', 'e2'),
        ]

        for call in mprint.call_args_list:
            arg, kw = call
            assert kw == {}
            assert arg == next(iter(expectations))
            expectations.pop(0)

    def test_scap_schedulexccdfscan_no_args(self, shell):
        """
        Test for do_scap_schedulexccdfscan with no args.

        :param shell:
        :return:
        """
        shell.help_scap_schedulexccdfscan = MagicMock()
        shell.ssm.keys = MagicMock()
        shell.expand_systems = MagicMock()
        shell.get_system_id = MagicMock()
        shell.client.system.scap.scheduleXccdfScan = MagicMock()
        mprint = MagicMock()

        for insufficient_param in ["", "ssm", "/tmp foo"]:
            with patch("spacecmd.scap.print", mprint):
                spacecmd.scap.do_scap_schedulexccdfscan(shell, insufficient_param)

            assert shell.help_scap_schedulexccdfscan.called
            assert not shell.ssm.keys.called
            assert not shell.expand_systems.called
            assert not shell.get_system_id.called
            assert not shell.client.system.scap.scheduleXccdfScan.called
            assert not mprint.called

    def test_scap_schedulexccdfscan_ssm_arg(self, shell):
        """
        Test for do_scap_schedulexccdfscan with SSM arg

        :param shell:
        :return:
        """
        shell.help_scap_schedulexccdfscan = MagicMock()
        shell.ssm.keys = MagicMock(return_value=[])
        shell.expand_systems = MagicMock()
        shell.get_system_id = MagicMock()
        shell.client.system.scap.scheduleXccdfScan = MagicMock()

        spacecmd.scap.do_scap_schedulexccdfscan(shell, "ssm /opt/xccdf.xml xccdf-option")

        assert not shell.help_scap_schedulexccdfscan.called
        assert shell.ssm.keys.called
        assert not shell.expand_systems.called
        assert not shell.get_system_id.called
        assert not shell.client.system.scap.scheduleXccdfScan.called

    def test_scap_schedulexccdfscan_systems_arg(self, shell):
        """
        Test for do_scap_schedulexccdfscan with systems arg

        :param shell:
        :return:
        """
        shell.help_scap_schedulexccdfscan = MagicMock()
        shell.ssm.keys = MagicMock()
        shell.expand_systems = MagicMock()
        shell.get_system_id = MagicMock()
        shell.client.system.scap.scheduleXccdfScan = MagicMock()

        spacecmd.scap.do_scap_schedulexccdfscan(shell, "/opt/xccdf.xml xccdf-option some.example.com")

        assert not shell.help_scap_schedulexccdfscan.called
        assert not shell.ssm.keys.called
        assert shell.expand_systems.called
        assert not shell.get_system_id.called
        assert not shell.client.system.scap.scheduleXccdfScan.called

        assert_expect(shell.expand_systems.call_args_list, ["some.example.com"])

    def test_scap_schedulexccdfscan_systems_arg_with_data(self, shell):
        """
        Test for do_scap_schedulexccdfscan with systems arg with data

        :param shell:
        :return:
        """
        shell.help_scap_schedulexccdfscan = MagicMock()
        shell.ssm.keys = MagicMock()
        shell.expand_systems = MagicMock(return_value=["some.example.com"])
        shell.get_system_id = MagicMock(return_value="1000010000")
        shell.client.system.scap.scheduleXccdfScan = MagicMock()

        spacecmd.scap.do_scap_schedulexccdfscan(shell, "/opt/xccdf.xml xccdf-option some.example.com")

        assert not shell.help_scap_schedulexccdfscan.called
        assert not shell.ssm.keys.called
        assert shell.expand_systems.called
        assert shell.get_system_id.called
        assert shell.client.system.scap.scheduleXccdfScan.called

        for call in shell.client.system.scap.scheduleXccdfScan.call_args_list:
            args, kw = call
            assert args == (shell.session, '1000010000', '/opt/xccdf.xml', '--xccdf-option')
07070100000037000081B40000000000000000000000015DA8415F00002287000000000000000000000000000000000000001D00000000spacecmd/tests/test_shell.py# coding: utf-8
"""
Unit test for the spacecmd.shell module.
"""
from mock import MagicMock, patch
import os
import time
import readline
import pytest
from spacecmd.shell import SpacewalkShell, UnknownCallException


class TestSCShell:
    """
    Test shell in spacecmd.
    """
    @patch("spacecmd.shell.atexit", MagicMock())
    def test_shell_history(self):
        """
        Test history length.
        """
        assert SpacewalkShell.HISTORY_LENGTH == 1024

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims",
           MagicMock(return_value=readline.get_completer_delims()))
    @patch("spacecmd.shell.sys.exit", MagicMock())
    def test_shell_delimeters(self):
        """
        Test shell delimieters are set without hyphens
        or colons during the tab completion.
        """
        cfg_dir = "/tmp/shell/{}/conf".format(int(time.time()))
        m_logger = MagicMock()

        cpl_setter = MagicMock()
        with patch("spacecmd.shell.logging", m_logger) as lgr, \
            patch("spacecmd.shell.readline.set_completer_delims", cpl_setter):
            options = MagicMock()
            options.nohistory = True
            shell = SpacewalkShell(options, cfg_dir, None)

            assert shell.history_file == "{}/history".format(cfg_dir)
            assert not m_logger.error.called
            assert cpl_setter.call_args[0][0] != readline.get_completer_delims()
            assert cpl_setter.call_args[0][0] == ' \t\n`~!@#$%^&*()=+[{]}\\|;\'",<>?'

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims",
           MagicMock(return_value=readline.get_completer_delims()))
    @patch("spacecmd.shell.sys.exit", MagicMock())
    @patch("spacecmd.shell.os.path.isfile", MagicMock(side_effect=IOError("No such file")))
    def test_shell_no_history_file(self):
        """
        Test shell no history file should capture IOError and log it.
        """
        cfg_dir = "/tmp/shell/{}/conf".format(int(time.time()))
        m_logger = MagicMock()
        cpl_setter = MagicMock()
        with patch("spacecmd.shell.logging", m_logger):
            options = MagicMock()
            options.nohistory = False
            shell = SpacewalkShell(options, cfg_dir, None)

            assert shell.history_file == "{}/history".format(cfg_dir)
            assert not os.path.exists(shell.history_file)
            assert m_logger.error.call_args[0][0] == "Could not read history file"

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.print", MagicMock())
    @patch("spacecmd.shell.sys.exit", MagicMock(side_effect=Exception("Exit attempt")))
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_exit_keywords(self):
        """
        Test 'precmd' method of the shell on exit keywords.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        for cmd in ["exit", "quit", "eof"]:
            with pytest.raises(Exception) as exc:
                shell.precmd(cmd)
            assert "Exit attempt" in str(exc)

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_common_keywords(self):
        """
        Test 'precmd' method of the shell on common keywords, e.g. login, logout, clear etc.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        for cmd in ["help", "login", "logout", "whoami", "history", "clear"]:
            assert shell.precmd(cmd) == cmd

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_empty_line(self):
        """
        Test 'precmd' method of the shell on empty line.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        assert shell.precmd("") == ""

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_session_login(self):
        """
        Test 'precmd' method of the shell on session login.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        shell.do_login = MagicMock(side_effect=Exception("login attempt"))

        with pytest.raises(Exception) as exc:
            shell.precmd("system_list")
        assert "login attempt" in str(exc)

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_help_keyword(self):
        """
        Test 'precmd' method of the shell on --help/-h arguments.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        shell.session = True

        assert shell.precmd("system_list --help") == "help system_list"
        assert shell.precmd("system_list -h") == "help system_list"

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_one_char_cmd(self):
        """
        Test 'precmd' method one char.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        shell.session = True

        assert shell.precmd("x") == "x"

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.print", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_history_item", MagicMock(return_value="repeated item"))
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_precmd_history(self):
        """
        Test 'precmd' method getting history item.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        shell.session = True

        assert shell.precmd("!!") == "repeated item"

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.SpacewalkShell.print_result", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_postcmd(self):
        """
        Test 'postcmd' method of the shell.
        """
        options = MagicMock()
        options.nohistory = True
        shell = SpacewalkShell(options, "", None)
        shell.config["server"] = ""
        shell.session = True
        shell.ssm = {1: "one"}
        shell.postcmd("result", "command")
        assert shell.prompt == "spacecmd {SSM:1}> "

    @patch("spacecmd.shell.atexit", MagicMock())
    @patch("spacecmd.shell.readline.set_completer_delims", MagicMock())
    @patch("spacecmd.shell.readline.get_completer_delims", MagicMock(return_value=readline.get_completer_delims()))
    def test_shell_default(self):
        """
        Test 'default' method of the shell.
        """
        cmd = MagicMock()
        with patch("spacecmd.shell.Cmd.default", cmd):
            options = MagicMock()
            options.nohistory = True
            shell = SpacewalkShell(options, "", None)
            shell.config["server"] = ""
            shell.session = True

            with pytest.raises(UnknownCallException):
                shell.default("test")
            assert cmd.call_args[0][1] == "test"
07070100000038000081B40000000000000000000000015DA8415F00003A72000000000000000000000000000000000000001F00000000spacecmd/tests/test_snippet.py# coding: utf-8
"""
Test suite for snippet source
"""
from mock import MagicMock, patch, mock_open
from spacecmd import snippet
from helpers import shell, assert_expect
import pytest


class TestSCSnippets:
    """
    Test for snippet API.
    """
    def test_snippet_list_noarg(self, shell):
        """
        Test snippet list noargs
        """
        snippets = [
            {"name": "snippet - 3"},
            {"name": "snippet - 1"},
            {"name": "snippet - 2"},
        ]

        mprint = MagicMock()
        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        with patch("spacecmd.snippet.print", mprint):
            out = snippet.do_snippet_list(shell, "")

        assert out is None
        assert mprint.call_args_list[0][0] == ('snippet - 1\nsnippet - 2\nsnippet - 3',)  # Sorted

    def test_snippet_list_args(self, shell):
        """
        Test snippet list with the args
        """
        snippets = [
            {"name": "snippet - 3"},
            {"name": "snippet - 1"},
            {"name": "snippet - 2"},
        ]

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)

        mprint = MagicMock()
        with patch("spacecmd.snippet.print", mprint):
            out = snippet.do_snippet_list(shell, "", doreturn=True)

        assert out is not None
        assert out == ['snippet - 1', 'snippet - 2', 'snippet - 3']  # Sorted

    def test_snippet_details_noarg(self, shell):
        """
        Test snippet details no args
        """
        snippets = [
            {"name": "snippet - 3"},
            {"name": "snippet - 1"},
            {"name": "snippet - 2"},
        ]
        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.help_snippet_details = MagicMock()

        mprint = MagicMock()
        logger = MagicMock()
        with patch("spacecmd.snippet.print", mprint) as mpr, patch("spacecmd.snippet.logging", logger) as lgr:
            snippet.do_snippet_details(shell, "")
        assert not mprint.called
        assert not logger.warning.called
        assert shell.help_snippet_details.called

    def test_snippet_details_args(self, shell):
        """
        Test snippet details with the args
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]
        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.SEPARATOR = "---"
        shell.help_snippet_details = MagicMock()

        mprint = MagicMock()
        logger = MagicMock()
        with patch("spacecmd.snippet.print", mprint) as mpr, patch("spacecmd.snippet.logging", logger) as lgr:
            snippet.do_snippet_details(shell, "snippet4 snippet5 snippet3 snippet1")
        assert not shell.help_snippet_details.called
        assert logger.warning.called
        assert mprint.called

        calls = [
            "snippet4 is not a valid snippet",
            "snippet5 is not a valid snippet",
        ]
        assert_expect(logger.warning.call_args_list, *calls)

        stdout_data = [
            'Name:   snippet3', 'Macro:  3rd fragment',
            'File:   /tmp/3', '', 'three', '---',
            'Name:   snippet1', 'Macro:  1st fragment',
            'File:   /tmp/1', '', 'one',
        ]
        assert_expect(mprint.call_args_list, *stdout_data)

    def test_snippet_create_no_args(self, shell):
        """
        Test create snippet with no arguments and no name update.
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]

        prompt_user = MagicMock(side_effect=["custom-snippet", "/tmp/cs.snip"])
        logger = MagicMock()
        editor = MagicMock(return_value=("editor content", False))
        read_file = MagicMock(return_value="file content")
        mprint = MagicMock()

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.client.kickstart.snippet.createOrUpdate = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        with patch("spacecmd.snippet.logging", logger) as lgr, \
             patch("spacecmd.snippet.prompt_user", prompt_user) as prmt, \
             patch("spacecmd.snippet.editor", editor) as edtr, \
             patch("spacecmd.snippet.read_file", read_file) as rfl, \
             patch("spacecmd.snippet.print", mprint) as prn:
            snippet.do_snippet_create(shell, "")

        assert not logger.error.called
        assert not shell.client.kickstart.snippet.listCustom.called
        assert shell.user_confirm.called
        assert shell.client.kickstart.snippet.createOrUpdate.called

        assert_expect(prompt_user.call_args_list, "Name:", "File:")
        assert_expect(mprint.call_args_list, "", "Snippet: custom-snippet", "Contents", "--------", "file content")

    def test_snippet_create_name_arg(self, shell):
        """
        Test create snippet with only name argument
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]

        prompt_user = MagicMock(side_effect=["custom-snippet", "/tmp/cs.snip"])
        logger = MagicMock()
        editor = MagicMock(return_value=("editor content", False))
        read_file = MagicMock(return_value="file content")
        mprint = MagicMock()

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.client.kickstart.snippet.createOrUpdate = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        with patch("spacecmd.snippet.logging", logger) as lgr, \
             patch("spacecmd.snippet.prompt_user", prompt_user) as prmt, \
             patch("spacecmd.snippet.editor", editor) as edtr, \
             patch("spacecmd.snippet.read_file", read_file) as rfl, \
             patch("spacecmd.snippet.print", mprint) as prn:
            snippet.do_snippet_create(shell, "--name something")

        assert not shell.client.kickstart.snippet.listCustom.called
        assert not shell.client.kickstart.snippet.createOrUpdate.called
        assert not shell.user_confirm.called
        assert not mprint.called
        assert not prompt_user.called
        assert logger.error.called
        assert logger.error.call_args_list[0][0][0] == "A file is required"

    def test_snippet_create_file_arg(self, shell):
        """
        Test create snippet with only file argument
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]

        prompt_user = MagicMock(side_effect=["custom-snippet", "/tmp/cs.snip"])
        logger = MagicMock()
        editor = MagicMock(return_value=("editor content", False))
        read_file = MagicMock(return_value="file content")
        mprint = MagicMock()

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.client.kickstart.snippet.createOrUpdate = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        with patch("spacecmd.snippet.logging", logger) as lgr, \
             patch("spacecmd.snippet.prompt_user", prompt_user) as prmt, \
             patch("spacecmd.snippet.editor", editor) as edtr, \
             patch("spacecmd.snippet.read_file", read_file) as rfl, \
             patch("spacecmd.snippet.print", mprint) as prn:
            snippet.do_snippet_create(shell, "--file /path/to/somewhere.snip")

        assert not shell.client.kickstart.snippet.listCustom.called
        assert not shell.client.kickstart.snippet.createOrUpdate.called
        assert not shell.user_confirm.called
        assert not mprint.called
        assert not prompt_user.called
        assert logger.error.called
        assert logger.error.call_args_list[0][0][0] == "A name is required for the snippet"

    def test_snippet_create_args(self, shell):
        """
        Test create snippet with arguments.
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]

        prompt_user = MagicMock(side_effect=["custom-snippet", "/tmp/cs.snip"])
        logger = MagicMock()
        editor = MagicMock(return_value=("editor content", False))
        read_file = MagicMock(return_value="file content")
        mprint = MagicMock()

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.client.kickstart.snippet.createOrUpdate = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        with patch("spacecmd.snippet.logging", logger) as lgr, \
             patch("spacecmd.snippet.prompt_user", prompt_user) as prmt, \
             patch("spacecmd.snippet.editor", editor) as edtr, \
             patch("spacecmd.snippet.read_file", read_file) as rfl, \
             patch("spacecmd.snippet.print", mprint) as prn:
            snippet.do_snippet_create(shell, "--name foobar --file /path/to/somewhere.snip")

        assert not logger.error.called
        assert not shell.client.kickstart.snippet.listCustom.called
        assert not prompt_user.called
        assert shell.client.kickstart.snippet.createOrUpdate.called
        assert shell.user_confirm.called
        assert mprint.called
        assert_expect(mprint.call_args_list, "", "Snippet: foobar", "Contents", "--------", "file content")

    def test_snippet_create_editor(self, shell):
        """
        Test create snippet using the editor.
        """
        snippets = [
            {"name": "snippet3", "contents": "three", "fragment": "3rd fragment", "file": "/tmp/3"},
            {"name": "snippet1", "contents": "one", "fragment": "1st fragment", "file": "/tmp/1"},
            {"name": "snippet2", "contents": "two", "fragment": "2nd fragment", "file": "/tmp/2"},
        ]

        prompt_user = MagicMock(side_effect=["custom-snippet", "/tmp/cs.snip"])
        logger = MagicMock()
        editor = MagicMock(return_value=("editor content", False))
        read_file = MagicMock(return_value="file content")
        mprint = MagicMock()

        shell.client.kickstart.snippet.listCustom = MagicMock(return_value=snippets)
        shell.client.kickstart.snippet.createOrUpdate = MagicMock()
        shell.user_confirm = MagicMock(side_effect=[False, True])

        with patch("spacecmd.snippet.logging", logger) as lgr, \
             patch("spacecmd.snippet.prompt_user", prompt_user) as prmt, \
             patch("spacecmd.snippet.editor", editor) as edtr, \
             patch("spacecmd.snippet.read_file", read_file) as rfl, \
             patch("spacecmd.snippet.print", mprint) as prn:
            snippet.do_snippet_create(shell, "")

        assert not logger.error.called
        assert not shell.client.kickstart.snippet.listCustom.called
        assert prompt_user.called
        assert shell.client.kickstart.snippet.createOrUpdate.called
        assert shell.user_confirm.called
        assert mprint.called
        assert editor.called
        assert_expect(mprint.call_args_list, "", "Snippet: custom-snippet", "Contents", "--------", "editor content")

    def test_snippet_update_no_args(self, shell):
        """
        Test update snippet with no args
        """
        shell.do_snippet_create = MagicMock()
        shell.help_snippet_update = MagicMock()

        out = snippet.do_snippet_update(shell, "")
        assert shell.help_snippet_update.called
        assert out is None

    def test_snippet_update_args(self, shell):
        """
        Test update snippet with args
        """
        shell.do_snippet_create = MagicMock()
        shell.help_snippet_update = MagicMock()

        out = snippet.do_snippet_update(shell, "custom_name")

        assert not shell.help_snippet_update.called
        assert out is not None
        assert shell.do_snippet_create.called
        assert not shell.do_snippet_create.call_args_list[0][0][0]
        assert "update_name" in shell.do_snippet_create.call_args_list[0][1]
        assert shell.do_snippet_create.call_args_list[0][1]["update_name"] == "custom_name"

    def test_snippet_delete_no_args(self, shell):
        """
        Test delete snippet with no args
        """
        shell.client.kickstart.snippet.delete = MagicMock()
        shell.help_snippet_delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        out = snippet.do_snippet_delete(shell, "")

        assert not shell.client.kickstart.snippet.delete.called
        assert shell.help_snippet_delete.called
        assert out is None

    def test_snippet_delete_args(self, shell):
        """
        Test delete snippet with args (snippet name)
        """
        shell.client.kickstart.snippet.delete = MagicMock()
        shell.help_snippet_delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=True)

        out = snippet.do_snippet_delete(shell, "some-snippet")

        assert shell.client.kickstart.snippet.delete.called
        assert not shell.help_snippet_delete.called
        assert out is None
        assert shell.client.kickstart.snippet.delete.call_args_list[0][0][0] == shell.session
        assert shell.client.kickstart.snippet.delete.call_args_list[0][0][1] == "some-snippet"

    def test_snippet_delete_args_no_confirm(self, shell):
        """
        Test delete snippet with args (snippet name), unconfirmed.
        """
        shell.client.kickstart.snippet.delete = MagicMock()
        shell.help_snippet_delete = MagicMock()
        shell.user_confirm = MagicMock(return_value=False)

        out = snippet.do_snippet_delete(shell, "some-snippet")

        assert not shell.client.kickstart.snippet.delete.called
        assert not shell.help_snippet_delete.called
        assert out is None
07070100000039000081B40000000000000000000000015DA8415F00002D1A000000000000000000000000000000000000001B00000000spacecmd/tests/test_ssm.py# coding: utf-8
"""
Test suite for the SSM module commands.
"""

from mock import MagicMock, patch, mock_open
from spacecmd import ssm
from helpers import shell, assert_expect
import pytest


class TestSCSSM:
    """
    Test for SSM module API.
    """
    def test_ssm_add_noarg(self, shell):
        """
        Test do_ssm_add no args.

        :param shell:
        :return:
        """
        shell.help_ssm_add = MagicMock()
        shell.expand_systems = MagicMock(return_value=[])
        shell.ssm = {}
        shell.get_system_id = MagicMock(return_value=None)

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_add(shell, "")

        assert not logger.warning.called
        assert not logger.debug.called
        assert shell.help_ssm_add.called

    def test_ssm_add_system_not_found(self, shell):
        """
        Test do_ssm_add a system that does not exists.

        :param shell:
        :return:
        """
        shell.help_ssm_add = MagicMock()
        shell.expand_systems = MagicMock(return_value=[])
        shell.ssm = {}
        shell.get_system_id = MagicMock(return_value=None)

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_add(shell, "example.com")

        assert logger.warning.called
        assert not logger.debug.called
        assert not shell.help_ssm_add.called

        assert_expect(logger.warning.call_args_list, "No systems found")

    def test_ssm_add_system_already_in_list(self, shell):
        """
        Test do_ssm_add a system that already in the list.

        :param shell:
        :return:
        """

        shell.help_ssm_add = MagicMock()
        shell.expand_systems = MagicMock(return_value=["example.com"])
        shell.ssm = {"example.com": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"
        shell.get_system_id = MagicMock(return_value=None)

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_add(shell, "example.com")

        assert logger.warning.called
        assert logger.debug.called
        assert not shell.help_ssm_add.called
        assert save_cache.called

        assert_expect(logger.warning.call_args_list, "example.com is already in the list")
        assert_expect(logger.debug.call_args_list, "Systems Selected: 1")

        for call in save_cache.call_args_list:
            args, kw = call
            assert not kw
            assert args == (shell.ssm_cache_file, shell.ssm)

    def test_ssm_add_system_new(self, shell):
        """
        Test do_ssm_add a new system.

        :param shell:
        :return:
        """

        shell.help_ssm_add = MagicMock()
        shell.expand_systems = MagicMock(return_value=["new.com"])
        shell.ssm = {"example.com": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"
        shell.get_system_id = MagicMock(return_value={"name": "new.com"})

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_add(shell, "new.com")

        assert not logger.warning.called
        assert not shell.help_ssm_add.called
        assert logger.debug.called
        assert save_cache.called

        exp = ["Added new.com", "Systems Selected: 2"]
        for call in logger.debug.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)

        for call in save_cache.call_args_list:
            args, kw = call
            assert not kw
            assert args == (shell.ssm_cache_file, shell.ssm)

    def test_ssm_intersect_noarg(self, shell):
        """
        Test do_ssm_intersect without arguments.

        :param shell:
        :return:
        """
        shell.help_ssm_intersect = MagicMock()
        shell.expand_systems = MagicMock(return_value=["new.com"])
        shell.ssm = {"example.com": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_intersect(shell, "")

        assert shell.help_ssm_intersect.called
        assert not logger.warning.called
        assert not logger.debug.called
        assert not save_cache.called

    def test_ssm_intersect_no_systems_found(self, shell):
        """
        Test do_ssm_intersect when no given systems found.

        :param shell:
        :return:
        """
        shell.help_ssm_intersect = MagicMock()
        shell.expand_systems = MagicMock(return_value=[])
        shell.ssm = {}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_intersect(shell, "unknown")

        assert not shell.help_ssm_intersect.called
        assert logger.warning.called
        assert not logger.debug.called
        assert not save_cache.called

        assert_expect(logger.warning.call_args_list, "No systems found")

    def test_ssm_intersect(self, shell):
        """
        Test do_ssm_intersect when no given systems found.

        :param shell:
        :return:
        """
        shell.help_ssm_intersect = MagicMock()
        shell.expand_systems = MagicMock(return_value=["existing.com"])
        shell.ssm = {
            "brexit.co.uk": {"name": "finished"},
            "existing.com": {"name": "keptalive"},
        }
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_intersect(shell, "existing.com")

        assert not shell.help_ssm_intersect.called
        assert not logger.warning.called
        assert logger.debug.called
        assert save_cache.called

        exp = ["existing.com is in both groups: leaving in SSM", "Systems Selected: 1"]
        for call in logger.debug.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)

        assert shell.ssm == {'existing.com': {'name': 'keptalive'}}

        for call in save_cache.call_args_list:
            args, kw = call
            assert not kw
            assert args == (shell.ssm_cache_file, shell.ssm)

    def test_ssm_remove_noarg(self, shell):
        """
        Test do_ssm_remove without arguments.

        :param shell:
        :return:
        """
        shell.help_ssm_remove = MagicMock()
        shell.expand_systems = MagicMock(return_value=["new.com"])
        shell.ssm = {"example.com": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_remove(shell, "")

        assert shell.help_ssm_remove.called
        assert not logger.warning.called
        assert not logger.debug.called
        assert not save_cache.called

    def test_ssm_remove_no_systems_found(self, shell):
        """
        Test do_ssm_remove without arguments.

        :param shell:
        :return:
        """
        shell.help_ssm_remove = MagicMock()
        shell.expand_systems = MagicMock(return_value=[])
        shell.ssm = {"example.com": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_remove(shell, "unknown")

        assert not shell.help_ssm_remove.called
        assert logger.warning.called
        assert not logger.debug.called
        assert not save_cache.called

        assert_expect(logger.warning.call_args_list, "No systems found")

    def test_ssm_remove(self, shell):
        """
        Test do_ssm_remove without arguments.

        :param shell:
        :return:
        """
        shell.help_ssm_remove = MagicMock()
        shell.expand_systems = MagicMock(return_value=["remove.me"])
        shell.ssm = {"remove.me": {}, "keepalive.io": {}}
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        logger = MagicMock()
        save_cache = MagicMock()
        with patch("spacecmd.ssm.logging", logger) as lgr, \
            patch("spacecmd.ssm.save_cache", save_cache) as svc:
            ssm.do_ssm_remove(shell, "unknown")

        assert not shell.help_ssm_remove.called
        assert not logger.warning.called
        assert logger.debug.called
        assert save_cache.called
        assert shell.ssm == {'keepalive.io': {}}

        exp = ["Removed remove.me", "Systems Selected: 1"]
        for call in logger.debug.call_args_list:
            assert_expect([call], next(iter(exp)))
            exp.pop(0)

        for call in save_cache.call_args_list:
            args, kw = call
            assert not kw
            assert args == (shell.ssm_cache_file, shell.ssm)

    def test_ssm_list(self):
        """
        Test do_ssm_list for listing of the systems in the SSM group.

        :return:
        """
        shell.help_ssm_list = MagicMock()
        shell.ssm = {"remove.me": {}, "keepalive.io": {}}

        for args in ["unknown", ""]:
            logger = MagicMock()
            mprint = MagicMock()
            save_cache = MagicMock()
            with patch("spacecmd.ssm.logging", logger) as lgr, \
                patch("spacecmd.ssm.save_cache", save_cache) as svc, \
                patch("spacecmd.ssm.print", mprint) as prn:
                ssm.do_ssm_list(shell, args=args)

            assert len(shell.ssm) == 2
            assert mprint.called
            assert not save_cache.called
            assert_expect(mprint.call_args_list, "keepalive.io\nremove.me")

    def test_ssm_clear(self):
        """
        Test do_ssm_list for listing of the systems in the SSM group.

        :return:
        """
        shell.ssm_cache_file = "/tmp/ssm_cache_file"

        for args in ["unknown", ""]:
            shell.ssm = {"remove.me": {}, "keepalive.io": {}}
            logger = MagicMock()
            mprint = MagicMock()
            save_cache = MagicMock()
            with patch("spacecmd.ssm.logging", logger) as lgr, \
                patch("spacecmd.ssm.save_cache", save_cache) as svc, \
                patch("spacecmd.ssm.print", mprint) as prn:
                ssm.do_ssm_clear(shell, args=args)

            assert not shell.ssm
            assert not mprint.called
            assert not logger.warning.called
            assert not logger.debug.called
            assert save_cache.called

            for call in save_cache.call_args_list:
                args, kw = call
                assert not kw
                assert args == (shell.ssm_cache_file, shell.ssm)
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!
openSUSE Build Service is sponsored by