Cleaned out unused files and renamed the files to match the name change in the spec
--- .gitignore | 2 +- README | 2 +- agent/COPYING | 158 ++++ agent/Makefile | 94 ++ agent/aeolus-audrey-agent.spec.in | 100 +++ agent/audrey_agent.in.py | 1353 ++++++++++++++++++++++++++++++ agent/test_audrey_startup.py | 692 +++++++++++++++ audrey_start/COPYING | 158 ---- audrey_start/Makefile | 94 -- audrey_start/aeolus-audrey-agent.spec.in | 100 --- audrey_start/audrey_startup.in.py | 1353 ------------------------------ audrey_start/test_audrey_startup.py | 692 --------------- 12 files changed, 2399 insertions(+), 2399 deletions(-) create mode 100644 agent/COPYING create mode 100644 agent/Makefile create mode 100644 agent/aeolus-audrey-agent.spec.in create mode 100755 agent/audrey_agent.in.py create mode 100644 agent/test_audrey_startup.py delete mode 100644 audrey_start/COPYING delete mode 100644 audrey_start/Makefile delete mode 100644 audrey_start/aeolus-audrey-agent.spec.in delete mode 100755 audrey_start/audrey_startup.in.py delete mode 100644 audrey_start/test_audrey_startup.py
diff --git a/.gitignore b/.gitignore index f8f7101..a566b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.swp configserver/pkg/* -audrey_start/pkg/* +agent/pkg/* job*.cnd *-*-*-*-* *.pyc diff --git a/README b/README index e0854d4..90da2b7 100644 --- a/README +++ b/README @@ -27,7 +27,7 @@ Required to build: help2man
Summary of how to build the software: - % cd <repo>/audrey_start + % cd <repo>/agent % make rpms % cd <repo>/configserver % rake rpm diff --git a/agent/COPYING b/agent/COPYING new file mode 100644 index 0000000..35907a1 --- /dev/null +++ b/agent/COPYING @@ -0,0 +1,158 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, +and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the +copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other +entities that control, are controlled by, or are under common control with +that entity. For the purposes of this definition, "control" means (i) the +power, direct or indirect, to cause the direction or management of such +entity, whether by contract or otherwise, or (ii) ownership of fifty percent +(50%) or more of the outstanding shares, or (iii) beneficial ownership +of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions +granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation +or translation of a Source form, including but not limited to compiled +object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, +made available under the License, as indicated by a copyright notice that +is included in or attached to the work (an example is provided in the Appendix +below). + +"Derivative Works" shall mean any work, whether in Source or Object form, +that is based on (or derived from) the Work and for which the editorial +revisions, annotations, elaborations, or other modifications represent, +as a whole, an original work of authorship. For the purposes of this License, +Derivative Works shall not include works that remain separable from, or +merely link (or bind by name) to the interfaces of, the Work and Derivative +Works thereof. + +"Contribution" shall mean any work of authorship, including the original +version of the Work and any modifications or additions to that Work or +Derivative Works thereof, that is intentionally submitted to Licensor for +inclusion in the Work by the copyright owner or by an individual or Legal +Entity authorized to submit on behalf of the copyright owner. For the purposes +of this definition, "submitted" means any form of electronic, verbal, or +written communication sent to the Licensor or its representatives, including +but not limited to communication on electronic mailing lists, source code +control systems, and issue tracking systems that are managed by, or on +behalf of, the Licensor for the purpose of discussing and improving the +Work, but excluding communication that is conspicuously marked or otherwise +designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on +behalf of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source +or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable (except as stated in +this section) patent license to make, have made, use, offer to sell, sell, +import, and otherwise transfer the Work, where such license applies only +to those patent claims licensable by such Contributor that are necessarily +infringed by their Contribution(s) alone or by combination of their Contribution(s) +with the Work to which such Contribution(s) was submitted. If You institute +patent litigation against any entity (including a cross-claim or counterclaim +in a lawsuit) alleging that the Work or a Contribution incorporated within +the Work constitutes direct or contributory patent infringement, then any +patent licenses granted to You under this License for that Work shall terminate +as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work +or Derivative Works thereof in any medium, with or without modifications, and +in Source or Object form, provided that You meet the following conditions: + +1. You must give any other recipients of the Work or Derivative Works +a copy of this License; and +2. You must cause any modified files to carry prominent notices stating +that You changed the files; and +3. You must retain, in the Source form of any Derivative Works that +You distribute, all copyright, patent, trademark, and attribution notices +from the Source form of the Work, excluding those notices that do not pertain +to any part of the Derivative Works; and +4. If the Work includes a "NOTICE" text file as part of its distribution, +then any Derivative Works that You distribute must include a readable copy +of the attribution notices contained within such NOTICE file, excluding +those notices that do not pertain to any part of the Derivative Works, +in at least one of the following places: within a NOTICE text file distributed +as part of the Derivative Works; within the Source form or documentation, +if provided along with the Derivative Works; or, within a display generated +by the Derivative Works, if and wherever such third-party notices normally +appear. The contents of the NOTICE file are for informational purposes +only and do not modify the License. You may add Your own attribution notices +within Derivative Works that You distribute, alongside or as an addendum +to the NOTICE text from the Work, provided that such additional attribution +notices cannot be construed as modifying the License. You may add Your +own copyright statement to Your modifications and may provide additional +or different license terms and conditions for use, reproduction, or distribution +of Your modifications, or for any such Derivative Works as a whole, provided +Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, +any Contribution intentionally submitted for inclusion in the Work by You +to the Licensor shall be under the terms and conditions of this License, +without any additional terms or conditions. Notwithstanding the above, +nothing herein shall supersede or modify the terms of any separate license +agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade +names, trademarks, service marks, or product names of the Licensor, except +as required for reasonable and customary use in describing the origin of +the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable +law or agreed to in writing, Licensor provides the Work (and each Contributor +provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied, including, without limitation, +any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, +or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining +the appropriateness of using or redistributing the Work and assume any +risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether +in tort (including negligence), contract, or otherwise, unless required +by applicable law (such as deliberate and grossly negligent acts) or agreed +to in writing, shall any Contributor be liable to You for damages, including +any direct, indirect, special, incidental, or consequential damages of +any character arising as a result of this License or out of the use or +inability to use the Work (including but not limited to damages for loss +of goodwill, work stoppage, computer failure or malfunction, or any and +all other commercial damages or losses), even if such Contributor has been +advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the +Work or Derivative Works thereof, You may choose to offer, and charge a +fee for, acceptance of support, warranty, indemnity, or other liability +obligations and/or rights consistent with this License. However, in accepting +such obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any +liability incurred by, or claims asserted against, such Contributor by +reason of your accepting any such warranty or additional liability. diff --git a/agent/Makefile b/agent/Makefile new file mode 100644 index 0000000..537ab20 --- /dev/null +++ b/agent/Makefile @@ -0,0 +1,94 @@ +# +# Copyright [2011] [Red Hat, Inc.] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +AUDREY_STARTUP_CACHE_DIR ?= $(HOME)/audrey-agent-cache + +VERSION = 0.4.2 + +# For Release: 0..., set _audrey-agent_dev=1 so that we get extra_release.GIT- +# annotated rpm version strings. +_audrey-agent_dev = $(shell grep -q '^[[:space:]]*Release:[[:space:]]*0' \ + aeolus-audrey-agent.spec.in && echo 1 || :) + +# use $(shell...) here to collect the git head and date *once* per make target. +# that ensures that if multiple actions happen in the same target (like the +# multiple RPM builds in the rpms target), they all use the same date +git_head = $(shell git log -1 --pretty=format:%h) +date = $(shell date --utc +%Y%m%d%H%M%S) +GIT_RELEASE = $(date)git$(git_head) +RPMDIR = $$(rpm --eval '%{_rpmdir}') +RELEASE = $(shell grep ^Release: aeolus-audrey-agent.spec.in | \ + sed -e 's/Release:[ , ]*//' | sed -e 's/%.*//') +# RPM_FLAGS = --define "audrey-agent_cache_dir $(AUDREY_STARTUP_CACHE_DIR)" +# RPM_FLAGS += $(if $(_audrey-agent_dev),--define "extra_release .$(GIT_RELEASE)") +man_section = 8 + +# Create the dist. +# Run the audrey_agent.py audrey.$(man_section) then: +# Set the version numbers in aeolus-audrey-agent.spec and audrey_agent.py +# Copy the desired bits to the dist directory. +# tar up the contents of the dist directory. +dist: audrey_agent.py audrey.$(man_section) + rm -rf dist + sed -e 's/@VERSION@/$(VERSION)/' aeolus-audrey-agent.spec.in \ + > aeolus-audrey-agent.spec + mkdir -p dist/aeolus-audrey-agent-$(VERSION) + cp -a aeolus-audrey-agent.spec aeolus-audrey-agent.spec.in \ + COPYING Makefile audrey_agent.in.py \ + audrey.$(man_section) \ + test_audrey_agent.py dist/aeolus-audrey-agent-$(VERSION) + tar -C dist -zcvf aeolus-audrey-agent-$(VERSION).tar.gz aeolus-audrey-agent-$(VERSION) + +# cp audrey_agent.in.py to audrey_agent.py +audrey_agent.py: audrey_agent.in.py + sed -e 's/@VERSION@/$(VERSION)/' $< > $@-t + chmod a+x,a-w $@-t + mv $@-t $@ + +# cp audrey_starup.py to audrey. +audrey: audrey_agent.py + cp $< $@ + +# Generate the man page using help2man from the --help text output from the Audrey agent. +# Run the audrey target before geneeerating the man page. +audrey.$(man_section): audrey + help2man --name='Aeolus agent' --section $(man_section) \ + --no-info ./$< > $@-t + mv $@-t $@ + +# Run the automated tests +# Run the audrey_agent.py target before running the automated tests. +check: audrey_agent.py + python test_audrey_agent.py + +# Build the rpms +# Run the check target followed by the dist target before building the rpms. +rpms: check dist + rpmbuild -ta aeolus-audrey-agent-$(VERSION).tar.gz + +# Build the srpms +srpms: check dist + rpmbuild -ts aeolus-audrey-agent-$(VERSION).tar.gz + +# Clean up files generated by this Makefile +clean: + rm -rf dist aeolus-audrey-agent-$(VERSION).tar.gz \ + aeolus-audrey-agent.spec audrey.$(man_section) audrey \ + audrey.log audrey_agent.py audrey_agent.pyc \ + test_toolinglog test_toolinguser \ + toolinglog toolinguser + +.PHONY: dist check rpms srpms diff --git a/agent/aeolus-audrey-agent.spec.in b/agent/aeolus-audrey-agent.spec.in new file mode 100644 index 0000000..c6dc369 --- /dev/null +++ b/agent/aeolus-audrey-agent.spec.in @@ -0,0 +1,100 @@ +# +# Copyright [2011] [Red Hat, Inc.] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +Name: aeolus-audrey-agent +Version: @VERSION@ +Release: 12%{?dist} +Summary: The Aeolus Audrey Startup Agent + +Group: Applications/System +License: ASL 2.0 +URL: http://aeolusproject.org +Source0: http://joev.fedorapeople.org/audrey-agent/aeolus-audrey-agent-%%7Bversion%7D... + +# All of these are required for building since during the build +# process "audrey --help" is executed to generate the man page. +BuildRequires: help2man +BuildRequires: facter python-httplib2 python-oauth2 +%if (0%{?fedora} <= 14 || 0%{?rhel} <= 6) +BuildRequires: python-argparse +%endif + +Requires: facter python-httplib2 python-oauth2 +Requires: python-argparse +%if (0%{?fedora} <= 14 || 0%{?rhel} <= 6) +Requires: python-argparse +%endif + +BuildArch: noarch + +%description +The Aeolus Audrey Startup Agent, a script which runs on a booting +cloud instance to retrieve configuration data from the Aeolus +Config Server. + +%prep +%setup -q + +%build +make %{?_smp_mflags} + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot}%{_bindir} +mkdir -p %{buildroot}%{_mandir}/man8 + +# copy over the audrey agent bits +install audrey_agent.py %{buildroot}%{_bindir}/audrey +cp audrey.8 %{buildroot}%{_mandir}/man8 + +%files +%{_bindir}/audrey +%{_mandir}/man8/audrey.8* +%doc COPYING + +%changelog +* Thu Nov 18 2011 Joe VLcek jvlcek@redhat.com 0.4.0-12 +- Fixed Auto help file generation induced build failure +* Thu Nov 17 2011 Joe VLcek jvlcek@redhat.com 0.4.0-11 +- Remove Auto help file generation as it is currently breakin the build +* Thu Nov 17 2011 Joe VLcek jvlcek@redhat.com 0.4.0-10 +- Fix bz754769 fix user data parsing and log level +* Tue Nov 09 2011 Joe VLcek jvlcek@redhat.com 0.4.0-9 +- Update the licensing information +* Tue Nov 08 2011 Joe VLcek jvlcek@redhat.com 0.4.0-8 +- Add man page generation and address some packaging review feedback +* Tue Nov 08 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-7 +- Fixing changelog history +* Mon Nov 07 2011 Joe VLcek jvlcek@redhat.com 0.3.1-7 +- Address Packaging for Fedora review feedback +* Fri Nov 05 2011 Joe VLcek jvlcek@redhat.com 0.3.1-6 +- Packaging for Fedora +* Wed Nov 02 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-6 +- Handles base64 encoded and decoded data in user data +* Wed Nov 02 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-3 +- Fix for audrey.log location +* Thu Oct 27 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-2 +- Radez doubled unit test coverage +* Wed Oct 26 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-1 +- Now using oauth to authenticate with Config Server +- Parsing a new user data format that is versioned +* Thu Oct 20 2011 Dan Radez dradez@redhat.com 0.3.1-3 +- adding requires for rpm installations +* Tue Oct 11 2011 Joe VLcek joev@redhat.com 0.3.1-2 +- RHEVm user data injection base64 encoded. +* Fri Sep 30 2011 Joe VLcek joev@redhat.com 0.3.1-1 +- RHEVm user data injection. +* Wed May 18 2011 Joe VLcek joev@redhat.com 0.0.1-1 +- Initial build. diff --git a/agent/audrey_agent.in.py b/agent/audrey_agent.in.py new file mode 100755 index 0000000..f59ba1e --- /dev/null +++ b/agent/audrey_agent.in.py @@ -0,0 +1,1353 @@ +#! /usr/bin/env python +''' +* +* Copyright [2011] [Red Hat, Inc.] +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +''' + +''' +Audrey Startup (AS) + +Note: The source file is named audrey_agent.in.py The make process generates + audrey_agent.py. audrey_agent.py should not be manually modified. + +Invoked at instance launch to interface with the Config Server (CS) + +For prototype end to end testing this file needs to be installed +at: /usr/bin/audrey + +Algorithim: + Get CF info + Loop: + Get and validate required configuration from CS + Configure system using required configuration + Get and validate provides parameters from CS + Gather provided parameter data from system + Put provided parameter data to CS + if not done then goto Loop + +''' + +import argparse +import base64 +import httplib2 +import logging +import os +import shutil +import sys +import tarfile +import tarfile as tf # To simplify exception names. +import tempfile +import urllib +import oauth2 as oauth + +from time import sleep +from collections import deque +from subprocess import Popen, PIPE + +EC2_USER_DATA_URL = 'http://169.254.169.254/latest/user-data' +CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info' + +# Location of the config tooling. +TOOLING_DIR = '/var/audrey/tooling/' + +# Log file +LOG = '/var/log/audrey.log' +LOGGER = None +CS_API_VER = 1 +# The VERSION string is filled in during the make process. +AUDREY_VER = '@VERSION@' + +# When running on condor-cloud, the Config Server (CS) contact +# information will be stored in the smbios. +# These are the dmi files where the smbios information is stored. +CONDORCLOUD_CS_ADDR = '/sys/devices/virtual/dmi/id/sys_vendor' +CONDORCLOUD_CS_UUID = '/sys/devices/virtual/dmi/id/product_name' + +# +# Error Handling methods: +# +class ASError(Exception): + ''' + Some sort of error occurred. The exact cause of the error should + have been logged. So, this just indicates that something is wrong. + ''' + pass + +def _raise_ASError(err_msg): + ''' + Log an error message and raise ASError + ''' + LOGGER.error(err_msg) + raise ASError(err_msg) + +class _run_cmd_return_subproc(): + ''' + Used to pass return code to caller if no subprocess object + is generated by Popen() due to an error. + ''' + returncode = 127 + +# +# Misc. Supporting Methods +# +def _run_cmd(cmd, my_cwd=None): + ''' + Description: + Run a command given by a dictionary, check for stderr output, + return code. + + To check the return code catch SystemExit then examine: + ret['subproc'].returncode. + + Input: + + cmd - a list containing the command to execute. + e.g.: cmd = ['ls', '/tmp'] + + Returns: + + ret - a dictionary upon return contains the keys: + 'subproc', 'err', 'out' + + ret['subproc'].returncode = subprocess return code + ret['err'] = command errors string. + ret['out'] = command out list. + + Example: + + cmd = ['ls', '/tmp'] + ret = _run_cmd(cmd) + + ret.keys() + ['subproc', 'err', 'out'] + ret['subproc'].returncode + 0 + ret['err'] + '' + ret['out'] + + ''' + + pfail = _run_cmd_return_subproc() + + # Return dictionary to contain keys: 'cmd', 'subproc', 'err', 'out' + ret = {'subproc' : None, 'err' : '' , 'out' : ''} + + try: + ret['subproc'] = Popen(cmd, cwd=my_cwd, stdout=PIPE, stderr=PIPE) + + # unable to find command will result in an OSError + except OSError, err: + if not ret['subproc']: + ret['subproc'] = pfail + + ret['subproc'].returncode = 127 # command not found + ret['err'] = str(err) + return ret + + # fill ret['out'] with stdout and ret['err'] with stderr + ret.update(zip(['out', 'err'], ret['subproc'].communicate())) + + return ret + +def _run_pipe_cmd(cmd1, cmd2): + ''' + Description: + Run one command piped into another. Commands are given as + dictionaries, check for stderr output, return code. + + To check the return code catch SystemExit then examine: + ret['subproc'].returncode. + + That is this routine can be used to execute a command + of the form: + + Input: + + cmd1 - a list containing the command to execute. + e.g.: cmd = ['ls', '/tmp'] + + cmd2 - a list containing the command to pipe the output + of cmd1 to. + e.g.: cmd = ['grep', 'a_file'] + + Returns: + + ret - a dictionary upon return contains the keys: + 'subproc', 'err', 'out' + + ret['subproc'].returncode = subprocess return code + ret['err'] = command errors string. + ret['out'] = command out list. + + Example: + + cmd1 = ['ls', '/tmp'] + cmd2 = ['grep', 'a_file'] + ret = _run_pipe_cmd(cmd1, cmd2) + + ret.keys() + ['subproc', 'err', 'out'] + ret['subproc'].returncode + 0 + ret['err'] + '' + ret['out'] + + ''' + + # Return dictionary to contain keys: 'cmd', 'subproc', 'err', 'out' + ret = {'subproc' : None, 'err' : '' , 'out' : ''} + + p1 = None + p2 = None + pfail = _run_cmd_return_subproc() + + # Execute the first command: + try: + p1 = Popen(cmd1, stdout=PIPE) + p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE ) + p1.stdout.close() + + # fill ret['out'] with stdout and ret['err'] with stderr + # ret.update(zip(['out', 'err'], ret['subproc'].communicate()[0])) + ret.update(zip(['out', 'err'], p2.communicate())) + ret['subproc'] = p2 + + # unable to find command will result in an OSError + except OSError, err: + if p2: + ret['subproc'] = p2 + elif p1: + ret['subproc'] = p1 + else: + ret['subproc'] = pfail + + ret['subproc'].returncode = 127 # command not found + ret['err'] = str(err) + return ret + + return ret + +class ServiceParams(object): + ''' + Description: + Used for storing a service and all of it's associated parameters + as provided by the Config Server in the "required" parameters + API message. + + services = [ + ServiceParams('serviceA', ['n&v', 'n&v', 'n&v',...]), + ServiceParams('serviceB', ['n&v', 'n&v', 'n&v',...]), + ServiceParams('serviceB', ['n&v', 'n&v', 'n&v',...]), + ] + + This structure aids in tracking the parsed required config + parameters which is useful when doing UNITTESTing. + + ''' + def __init__(self, name=None): + if name == None: + name = '' + self.name = name # string + self.params = [] # start with an empty list + def add_param(self, param): + ''' + Description: + Add a parameter provided by the Config Server to the list. + ''' + self.params.append(param) + def __repr__(self): + return repr((self.name, self.params)) + +# +# Methods used to parse the CS<->AS text based API +# +def _common_validate_message(src): + ''' + Perform validation of the text message sent from the Config Server. + ''' + + if not src.startswith('|') or not src.endswith('|'): + _raise_ASError(('Invalid start and end characters: %s') % (src)) + +def gen_env(serv_name, param_val): + ''' + Description: + Generate the os environment variables from the required config string. + + Input: + serv_name - A service name + e.g.: + jon_agent_config + + param_val - A parameter name&val pair. The value is base64 encoded. + e.g.: + jon_server_ip&MTkyLjE2OC4wLjE= + + Output: + Set environment variables of the form: + <name>=<value> + e.g.: + jon_server_ip=base64.b64decode('MTkyLjE2OC4wLjE=') + jon_server_ip='192.168.0.1 + + Raises ASError when encountering an error. + + ''' + LOGGER.debug('Invoked gen_env()') + + # If the param_val is missing treat as an exception. + if param_val == '': + _raise_ASError(('Missing parameter name. %s') % \ + (str(param_val))) + + # If serv_name is not blank an extra "_" must be added to + # the environment variable name. + if serv_name != '': + serv_name = serv_name + '_' + + name_val = param_val.split('&') + var_name = 'AUDREY_VAR_' + serv_name + name_val[0] + os.environ[var_name] = \ + base64.b64decode(name_val[1]) + + # Get what was set and log it. + cmd = ['/usr/bin/printenv', var_name] + ret = _run_cmd(cmd) + LOGGER.debug(var_name + '=' + str(ret['out'].strip())) + +def parse_require_config(src): + ''' + Description: + Parse the required config text message sent from the Config Server. + + Input: + The required config string obtained from the Config Server, + delimited by an | and an & + + Two tags will mark the sections of the data, + '|service|' and '|parameters|' + + To ensure all the data was received the entire string will be + terminated with an "|". + + The string "|service|" will precede a service names. + + The string "|parameters|" will precede the parameters for + the preceeding service, in the form: names&<b64 encoded values>. + + This will be a continuous text string (no CR or New Line). + + Format (repeating for each service): + + |service|<s1>|parameters|name1&<b64val>|name2&<b64val>...|nameN&<b64v>| + + + e.g.: + |service|ssh::server|parameters|ssh_port&<b64('22')> + |service|apache2::common|apache_port&<b64('8081')>| + + Returns: + - A list of ServiceParams objects. + ''' + + services = [] + new = None + + _common_validate_message(src) + + # Message specific validation + if src == '||': + # special case indicating no required config needed. + return [] + + if src.find('|service|') != 0: + _raise_ASError(('|service| is not the first tag found. %s') % (src)) + + + src_q = deque(src.split('|')) + + # remove leading and trailing elements from the src_q since they are + # empty strings generated by the split('|') because of the leading + # and trailing '|' + token = src_q.popleft() + token = src_q.pop() + + while True: + try: + token = src_q.popleft() + if token == 'service': + token = src_q.popleft() # next token is service name + + # Raise an error if the service name is invalid. + if token.find('&') != -1 or \ + token == 'service' or \ + token == 'parameters': + _raise_ASError(('ERROR invalid service name: %s') % \ + (str(token))) + + new = ServiceParams(token) + services.append(new) + elif token == 'parameters' or token == '': + pass + else: # token is a name&value pair. + if token.find('&') == -1: + _raise_ASError(('ERROR name&val: %s missing delimiter') % \ + (str(token))) + if new: + new.add_param(token) + gen_env(new.name, token) + else: + _raise_ASError(('ERROR missing service tag %s') % \ + (str(src))) + except IndexError: + break + + return services + +def _get_system_info(): + ''' + Description: + Get the system info to be used for generating this instances + provides back to the Config Server. + + Currently utilizes Puppet's facter via a Python subprocess call. + + Input: + None + + Returns: + A dictionary of system info name/value pairs. + + ''' + + cmd = ['/usr/bin/facter'] + ret = _run_cmd(cmd) + if ret['subproc'].returncode != 0: + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + facts = {} + for fact in ret['out'].split('\n'): + if fact: # Handle the new line at the end of the facter output + name, val = fact.split(' => ') + facts[ name ] = val.rstrip() + + return facts + +def parse_provides_params(src): + ''' + Description: + Parse the provides parameters text message sent from the + Config Server. + + Input: + The provides parameters string obtained from the Config Server. + + The delimiters will be an | and an & + + To ensure all the data was received the entire string will be + terminated with an "|". + + This will be a continuous text string (no CR or New Line). + + Format: + |name1&name2...&nameN| + + e.g.: + |ipaddress&virtual| + + Returns: + - a list of parameter names. + ''' + + _common_validate_message(src) + + # Message specific validation + if src == '||': + # special case indicating no provides parameters requested. + return [''] + + params_str = src[src.find('|')+1:len(src)-1] + + return params_str.split('&') + +def generate_provides(src): + ''' + Description: + Generate the provides parameters list. + Uses parse_provides_params() + + Input: + The provides parameters string obtained from the Config Server. + + Returns: + A string to send back to the Config Server with prifix + 'audrey_data='<url encoded return data>' + + The return portion will be delimited with an | and an & + + To ensure all the data is transmitted the entire string will be + terminated with an "|". + + This will be a continuous text string (no CR or New Line). + + Data portion Format: + |name1&val1|name2&val...|nameN$valN| + + e.g.: + |ipaddress&<b64/10.118.46.205>|virtual&<b64/xenu>| + + The return string format: + "audrey_data=<url encoded data portion>" + + + ''' + LOGGER.info('Invoked generate_provides()') + + provides_dict = {} + params_list = parse_provides_params(src) + + system_info_dict = _get_system_info() + + for param in params_list: + try: + provides_dict.update( \ + {param:base64.b64encode(system_info_dict[param])}) + except KeyError: + # A specified parameter is not found. Provide value '' + provides_dict.update({param:''}) + + + # Create string to send to Config Server + provides_list = [''] + for key in provides_dict.keys(): + provides_list.append(str(key) + '&' + str(provides_dict[key])) + provides_list.append('') + + return urllib.urlencode({'audrey_data':'|'.join(provides_list)}) + +class ConfigTooling(object): + ''' + TBD - Consider making this class derived from dictionary or a mutable + mapping. + + Description: + Interface to configuration tooling: + - Getting optional user supplied tooling from CS + - Verify and Unpack optional user supplied tooling retrieved + from CS + - Is tooling for a given service user supplied + - Is tooling for a given service Red Hat supplied + - Find tooling for a given service Red Hat supplied + - List tooling for services and indicate if it is user or Red + Hat supplied. + ''' + + def __init__(self, tool_dir=TOOLING_DIR): + ''' + Description: + Set initial state so it can be tracked. Valuable for + testing and debugging. + ''' + self.tool_dir = tool_dir + self.user_dir = tool_dir + 'user/' + self.log = tool_dir + 'log' + self.tarball = '' + + # Create the extraction destination + try: + os.makedirs(self.user_dir) + except OSError, (errno, strerror): + if errno is 17: # File exists + pass + else: + _raise_ASError(('Failed to create directory %s. ' + \ + 'Error: %s') % (self.user_dir, strerror)) + + self.ct_logger = logging.getLogger('ConfigTooling') + self.ct_logger.addHandler(logging.FileHandler(self.log)) + + def __str__(self): + ''' + Description: + Called by the str() function and by the print statement to + produce the informal string representation of an object. + ''' + return('\n<Instance of: %s\n' \ + '\tTooling Dir: %s\n' \ + '\tUnpack User Tooling Tarball Dir: %s\n' \ + '\tLog File: %s\n' \ + '\ttarball Name: %s\n' \ + 'eot>' % + (self.__class__.__name__, + str(self.tool_dir), + str(self.user_dir), + str(self.log), + str(self.tarball), + )) + + def log_info(self, log_str): + ''' + Description: + Used for logging the commands that have been executed + along with their output and return codes. + + Simply logs the provided input string. + ''' + self.ct_logger.info(log_str) + + def log_error(self, log_str): + ''' + Description: + Used for logging errors encountered when attempting to + execute the service command. + + Simply logs the provided input string. + ''' + self.ct_logger.error(log_str) + + def invoke_tooling(self, services): + ''' + Description: + Invoke the configuration tooling for the specified services. + + Input: + services - A list of ServiceParams objects. + + ''' + + # For now invoke them all. Later versions will invoke the service + # based on the required params from the Config Server. + LOGGER.debug('Invoked ConfigTooling.invoke_tooling()') + LOGGER.debug(str(services)) + for service in services: + + try: + top_level, tooling_path = self.find_tooling(service.name) + except ASError: + # No tooling found. Try the next service. + continue + + cmd = [tooling_path] + cmd_dir = os.path.dirname(tooling_path) + ret = _run_cmd(cmd, cmd_dir) + self.log_info('Execute Tooling command: ' + ' '.join(cmd)) + + retcode = ret['subproc'].returncode + if retcode == 0: + # Command successed, log the output. + self.log_info('return code: ' + str(retcode)) + self.log_info('\n\tStart Output of: ' + ' '.join(cmd) + \ + ' >>>\n' + \ + str(ret['out']) + \ + '\n\t<<< End Output') + else: + # Command failed, log the errors. + self.log_info('\n\tStart Output of: ' + ' '.join(cmd) + \ + ' >>>\n' + \ + str(ret['out']) + \ + '\n\t<<< End Output') + self.log_error('error code: ' + str(retcode)) + self.log_error('error msg: ' + str(ret['err'])) + + # If tooling was provided at the top level only run it once + # for all services listed in the required config params. + if top_level: + break + + def unpack_tooling(self, tarball): + ''' + Description: + Methods used to untar the user provided tarball + + Perform validation of the text message sent from the + Config Server. Validate, open and write out the contents + of the user provided tarball. + ''' + LOGGER.info('Invoked unpack_tooling()') + LOGGER.debug('tarball: ' + str(tarball) + \ + 'Target Direcory: ' + str(self.user_dir)) + + self.tarball = tarball + + # Validate the specified tarfile. + try: + if not tarfile.is_tarfile(self.tarball): + # If file exists but is not a tar file force IOError. + raise IOError + except IOError, (errno, strerror): + _raise_ASError(('File was not found or is not a tar file: %s ' + \ + 'Error: %s %s') % (self.tarball, errno, strerror)) + + # Attempt to extract the contents from the specified tarfile. + # + # If tarfile access or content is bad report to the user to aid + # problem resolution. + try: + tarf = tarfile.open(self.tarball) + tarf.extractall(path=self.user_dir) + tarf.close() + except IOError, (errno, strerror): + _raise_ASError(('Failed to access tar file %s. Error: %s') % \ + (self.tarball, strerror)) + # Capture and report errors with the tarfile + except (tf.TarError, tf.ReadError, tf.CompressionError, \ + tf.StreamError, tf.ExtractError), (strerror): + + _raise_ASError(('Failed to access tar file %s. Error: %s') % \ + (self.tarball, strerror)) + + def is_user_supplied(self): + ''' + Description: + Is the the configuration tooling for the specified service + supplied by the user? + + TBD: Take in a service_name and evaluate. + def is_user_supplied(self, service_name): + ''' + return True + + def is_rh_supplied(self): + ''' + Description: + Is the the configuration tooling for the specified service + supplied by Red Hat? + + TBD: Take in a service_name and evaluate. + def is_rh_supplied(self, service_name): + ''' + return False + + def find_tooling(self, service_name): + ''' + Description: + Given a service name return the path to the configuration + tooling. + + Search for the service start executable in the user + tooling directory. + self.tool_dir + '/user/<service name>/start' + + If not found there search for the it in the documented directory + here built in tooling should be placed. + self.tool_dir + '/AUDREY_TOOLING/<service name>/start' + + If not found there search for the it in the Red Hat tooling + directory. + self.tool_dir + '/REDHAT/<service name>/start' + + If not found there raise an error. + + Returns: + return 1 - True if top level tooling found, False otherwise. + return 2 - path to tooling + ''' + + top_path = self.tool_dir + 'user/start' + if os.access(top_path, os.X_OK): + return True, top_path + + service_user_path = self.tool_dir + 'user/' + \ + service_name + '/start' + if os.access(service_user_path, os.X_OK): + return False, service_user_path + + service_redhat_path = self.tool_dir + 'AUDREY_TOOLING/' + \ + service_name + '/start' + if os.access(service_redhat_path, os.X_OK): + return False, service_redhat_path + + service_redhat_path = self.tool_dir + 'REDHAT/' + \ + service_name + '/start' + if os.access(service_redhat_path, os.X_OK): + return False, service_redhat_path + + # No tooling found. Raise an error. + _raise_ASError(('No configuration tooling found for service: %s') % \ + (service_name)) + +class CSClient(object): + ''' + Description: + Client interface to Config Server (CS) + ''' + + def __init__(self, endpoint, oauth_key, oauth_secret, **kwargs): + ''' + Description: + Set initial state so it can be tracked. Valuable for + testing and debugging. + ''' + + self.version = CS_API_VER + self.cs_endpoint = endpoint + self.cs_oauth_key = oauth_key + self.cs_oauth_secret = oauth_secret + self.ec2_user_data_url = EC2_USER_DATA_URL + self.cs_params = '' + self.cs_configs = '' + self.tmpdir = '' + self.tarball = '' + + # create an oauth client for communication with the cs + consumer = oauth.Consumer(self.cs_oauth_key, self.cs_oauth_secret) + # 2 legged auth, token unnessesary + token = None #oauth.Token('access-key-here','access-key-secret-here') + client = oauth.Client(consumer, token) + self.http = client + + def __del__(self): + ''' + Description: + Class destructor + ''' + try: + shutil.rmtree(self.tmpdir) + except OSError: + pass # ignore any errors when attempting to remove the temp dir. + + def __str__(self): + ''' + Description: + Called by the str() function and by the print statement to + produce the informal string representation of an object. + ''' + return('\n<Instance of: %s\n' \ + '\tVersion: %s\n' \ + '\tConfig Server Endpoint: %s\n' \ + '\tConfig Server oAuth Key: %s\n' \ + '\tConfig Server oAuth Secret: %s\n' \ + '\tConfig Server Params: %s\n' \ + '\tConfig Server Configs: %s\n' \ + '\tTemporary Directory: %s\n' \ + '\tTarball Name: %s\n' \ + 'eot>' % + (self.__class__.__name__, + str(self.version), + str(self.cs_endpoint), + str(self.cs_oauth_key), + str(self.cs_oauth_secret), + str(self.cs_params), + str(self.cs_configs), + str(self.tmpdir), + str(self.tarball), + )) + + def _cs_url(self, url_type): + ''' + Description: + Generate the Config Server (CS) URL. + ''' + return '%s/%s/%s/%s' % \ + (self.cs_endpoint, url_type, self.version, self.cs_oauth_key) + + def _get(self, url, headers=None): + ''' + Description: + Issue the http get to the the Config Server. + ''' + return self.http.request(url, method='GET', headers=headers) + + def _put(self, url, body=None, headers=None): + ''' + Description: + Issue the http put to the the Config Server. + ''' + return self.http.request(url, method='PUT', + body=body, headers=headers) + + def _validate_http_status(self, status): + ''' + Description: + Confirm the http status is one of: + 200 HTTP OK - Success and no more data of this type + 202 HTTP Accepted - Success and more data of this type + 404 HTTP Not Found - This may be temporary so try again + ''' + if (status != 200) and (status != 202) and (status != 404): + _raise_ASError(('Invalid HTTP status code: %s') % \ + (str(status))) + + # Public interfaces + def get_cs_configs(self): + ''' + Description: + get the required configuration from the Config Server. + ''' + LOGGER.info('Invoked CSClient.get_cs_configs()') + url = self._cs_url('configs') + headers = {'Accept': 'text/plain'} + + response, body = self._get(url, headers=headers) + self.cs_configs = body + self._validate_http_status(response.status) + + return response.status, body + + def get_cs_params(self): + ''' + Description: + get the provides parameters from the Config Server. + ''' + LOGGER.info('Invoked CSClient.get_cs_params()') + url = self._cs_url('params') + headers = {'Accept': 'text/plain'} + + response, body = self._get(url, headers=headers) + self.cs_params = body + self._validate_http_status(response.status) + + return response.status, body + + def put_cs_params_values(self, params_values): + ''' + Description: + put the provides parameters to the Config Server. + ''' + LOGGER.info('Invoked CSClient.put_cs_params_values()') + url = self._cs_url('params') + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + + response, body = self._put(url, body=params_values, headers=headers) + return response.status, body + + def get_cs_tooling(self): + ''' + Description: + get any optional user supplied tooling which is + provided as a tarball + ''' + LOGGER.info('Invoked CSClient.get_cs_tooling()') + url = self._cs_url('files') + headers = {'Accept': 'content-disposition'} + + tarball = '' + response, body = self._get(url, headers=headers) + self._validate_http_status(response.status) + + # Parse the file name burried in the response header + # at: response['content-disposition'] + # as: 'attachment; tarball="tarball.tgz"' + if (response.status == 200) or (response.status == 202): + tarball = response['content-disposition']. \ + lstrip('attachment; filename=').replace('"','') + + # Create the temporary tarfile + try: + self.tmpdir = tempfile.mkdtemp() + self.tarball = self.tmpdir + '/' + tarball + f = open(self.tarball, 'w') + f.write(body) + f.close() + except IOError, (errno, strerror): + _raise_ASError(('File not found or not a tar file: %s ' + \ + 'Error: %s %s') % (self.tarball, errno, strerror)) + + return response.status, self.tarball + +def discover_config_server(cloud_info_file=CLOUD_INFO_FILE, + condor_addr_file=CONDORCLOUD_CS_ADDR, + condor_uuid_file=CONDORCLOUD_CS_UUID, + ec2_user_data=EC2_USER_DATA_URL, + http=httplib2.Http()): + ''' + Description: + Discover the Config Server access info. + If not discover it using the cloud provider specific method. + ''' + # + # What Cloud Backend? + # + # Read the file populated with Cloud back end type. + # e.g.: CLOUD_TYPE="EC2" + # + + def _parse_user_data(data, condor=None): + ''' + Take a string in form version|cs_endpoint|oauth_key|oauth_secret + and populate the respective self vars. + Conductor puts the UUID into the oauth_key field. + At minimum this function expects to find a | in the string + this is in effort not to log oauth secrets. + ''' + LOGGER.debug('Parsing User Data') + user_data = data.split('|') + if len(user_data) > 1: + if user_data[0] == '1': + if condor: + ud_version, endpoint, \ + oauth_secret = user_data + oauth_key = condor + else: + ud_version, endpoint, \ + oauth_key, oauth_secret = user_data + return {'endpoint': endpoint, + 'oauth_key': oauth_key, + 'oauth_secret': oauth_secret,} + #elif ud[0] == nextversion + # parse code for version + else: + _raise_ASError('Invalid User Data Version: %s' % user_data[0]) + else: + _raise_ASError('Could not get user data version, parse failed') + + try: + with open(cloud_info_file, 'r') as fp: + read_data = fp.read() + except IOError: + _raise_ASError(('Failed accessing file %s') % \ + (cloud_info_file)) + + # + # Discover the Config Server access info. + # + cloud_type = read_data.upper() + if 'EC2' in cloud_type: + # + # If on EC2 the user data will contain the Config Server + # access info. + # + + try: + max_attempts = 5 + headers = {'Accept': 'text/plain'} + for attempt in range(1, max_attempts): + response, body = http.request(ec2_user_data, + headers=headers) + if response.status == 200: + break + if response.status != 200: + _raise_ASError('Max attempts to get EC2 user data \ + exceeded.') + + if '|' not in body: + body = base64.b64decode(body) + return _parse_user_data(body) + + except Exception, e: + _raise_ASError('Failed accessing EC2 user data: %s' % e) + + elif 'CONDORCLOUD' in cloud_type: + # + # If on Condor Cloud, the user data will be in smbios + # Uses the dmi files to access the stored smbios information. + # + try: + return _parse_user_data(open(condor_addr_file, 'r').read().strip(), + open(condor_uuid_file, 'r').read().strip()) + except Exception, e: + _raise_ASError('Failed accessing Config Server data: %s' % e) + + elif 'RHEV' in cloud_type: + # + # If on RHEV-M the user data will be contained on the + # floppy device in file deltacloud-user-data.txt. + # To access it: + # modprobe floppy + # mount /dev/fd0 /media + # read /media/deltacloud-user-data.txt + # + # Note: + # On RHEVm the deltacloud drive had been delivering the user + # data base64 decoded at one point that changed such that the + # deltacloud drive leaves the date base64 encoded. This + # Code segment will handle both base64 encoded and decoded + # user data. + # + # Since ':' is used as a field delimiter in the user data + # and is not a valid base64 char, if ':' is found assume + # the data is already base64 decoded. + # + # modprobe floppy + cmd = ['/sbin/modprobe', 'floppy'] + ret = _run_cmd(cmd) + if ret['subproc'].returncode != 0: + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + cmd = ['/bin/mkdir', '/media'] + ret = _run_cmd(cmd) + # If /media is already there (1) or any other error (0) + if (ret['subproc'].returncode != 1) and \ + (ret['subproc'].returncode != 0): + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + cmd = ['/bin/mount', '/dev/fd0', '/media'] + ret = _run_cmd(cmd) + # If /media is already mounted (32) or any other error (0) + if (ret['subproc'].returncode != 32) and \ + (ret['subproc'].returncode != 0): + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + try: + # Condfig Server (CS) address:port. + with open('/media/deltacloud-user-data.txt', 'r') as fp: + line = fp.read().strip() + if '|' not in line: + line = base64.b64decode(line) + return _parse_user_data(line) + except: + _raise_ASError('Failed accessing RHEVm user data.') + + elif 'VSPHERE' in cloud_type: + # + # If on vSphere the user data will be contained on the + # floppy device in file deltacloud-user-data.txt. + # To access it: + # mount /dev/fd0 /media + # read /media/deltacloud-user-data.txt + # + # Note: + # On vSphere the deltacloud drive had been delivering the user + # data base64 decoded at one point that changed such that the + # deltacloud drive leaves the date base64 encoded. This + # Code segment will handle both base64 encoded and decoded + # user data. + # + # Since ':' is used as a field delimiter in the user data + # and is not a valid base64 char, if ':' is found assume + # the data is already base64 decoded. + # + cmd = ['/bin/mkdir', '/media'] + ret = _run_cmd(cmd) + # If /media is already there (1) or any other error (0) + if (ret['subproc'].returncode != 1) and \ + (ret['subproc'].returncode != 0): + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + cmd = ['/bin/mount', '/dev/cdrom', '/media'] + ret = _run_cmd(cmd) + # If /media is already mounted (32) or any other error (0) + if (ret['subproc'].returncode != 32) and \ + (ret['subproc'].returncode != 0): + _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ + (' '.join(cmd), str(ret['err']))) + + try: + # Condfig Server (CS) address:port. + with open('/media/deltacloud-user-data.txt', 'r') as fp: + line = fp.read().strip() + if '|' not in line: + line = base64.b64decode(line) + return _parse_user_data(line) + except: + _raise_ASError('Failed accessing vSphere user data.') + +def setup_logging(level=logging.INFO, logfile_name=LOG): + ''' + Description: + Establish the output logging. + ''' + + global LOGGER + + # If not run as root create the log file in the current directory. + # This allows minimal functionality, e.g.: --help + if not os.geteuid() == 0: + logfile_name = './audrey.log' + + # set up logging + LOG_FORMAT = ('%(asctime)s - %(levelname)-8s: ' + '%(filename)s:%(lineno)d %(message)s') + LOG_LEVEL_INPUT = 5 + LOG_NAME_INPUT = 'INPUT' + + logging.basicConfig(filename=logfile_name, + level=level, filemode='w', format=LOG_FORMAT) + + logging.addLevelName(LOG_LEVEL_INPUT, LOG_NAME_INPUT) + + LOGGER = logging.getLogger('Audrey') + +def parse_args(): + ''' + Description: + Gather any Config Server access info optionally passed + on the command line. If being provided on the command + line all of it must be provided. + + oAuth Secret is prompted for and not allowed as an argument. + This is to avoid a ps on the system from displaying the + oAuth Secret argument. + + Return: + dict - of parser keys and values + ''' + desc_txt = 'The Aeolus Audrey Startup Agent, a script which ' + \ + 'runs on a booting cloud instance to retrieve ' + \ + 'configuration data from the Aeolus Config Server.' + + log_level_dict={'DEBUG' : logging.DEBUG, + 'INFO' : logging.INFO, + 'WARNING' : logging.WARNING, + 'ERROR' : logging.ERROR, + 'CRITICAL' : logging.CRITICAL} + + parser = argparse.ArgumentParser(description=desc_txt) + parser.add_argument('-e', '--endpoint', dest='endpoint', + required=False, help='Config Server endpoint url') + parser.add_argument('-k', '--key', dest='oauth_key', required=False, + help='oAuth Key. If specified prompt for the oAuth Secret.') + parser.add_argument('-p', '--pwd', action='store_true', default=False, + required=False, help='Log and look for configs in pwd',) + parser.add_argument('-L', '--log-level', dest='log_level', + required=False, default='INFO', help='Audrey Agent Logging Level', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']), + parser.add_argument('-V', '-v', '--version', dest='version', + action='store_true', default=False, required=False, + help='Displays the program's version number and exit.') + + args = parser.parse_args() + args.log_level = log_level_dict[args.log_level] + + if args.version: + print AUDREY_VER + sys.exit() + + if args.oauth_key: + # Prompt for oAuth secret so ps won't display it. + args.oauth_secret = raw_input('oAuth Secret: ') + + return args + +def audrey_script_main(client_http=None): + ''' + Description: + This script will be used on EC2 for configuring the running + instance based on Cloud Engine configuration supplied at + launch time in the user data. + + Config Server Status: + 200 HTTP OK - Success and no more data of this type + 202 HTTP Accepted - Success and more data of this type + 404 HTTP Not Found - This may be temporary so try again + ''' + # parse the args and setup logging + conf = parse_args() + if 'pwd' in conf and conf.pwd: + log_file = 'audrey.log' + tool_dir = 'tooling' + cloud_info = 'cloud_info' + else: + log_file = LOG + tool_dir = TOOLING_DIR + cloud_info = CLOUD_INFO_FILE + + setup_logging(level=conf.log_level, + logfile_name=log_file) + + if not conf.endpoint: + if client_http: + conf = discover_config_server(cloud_info_file=cloud_info, + http=client_http) + else: + # discover the cloud I'm on + conf = discover_config_server(cloud_info_file=cloud_info) + + # ensure the conf it a dictionary, not a namespace + if hasattr(conf, '__dict__'): + conf = vars(conf) + + LOGGER.info('Invoked audrey_script_main') + + # 0 means don't run again + # -1 is non zero so initial runs will happen + config_status = -1 + param_status = -1 + tooling_status = -1 + + max_retry = 5 + services = [] + + # Create the Client Object + cs_client = CSClient(**conf) + if client_http: + cs_client.http = client_http + LOGGER.info(str(cs_client)) + + LOGGER.debug('Get optional tooling from the Config Server') + # Get any optional tooling from the Config Server + tooling = ConfigTooling(tool_dir=tool_dir) + tooling_status, tarball = cs_client.get_cs_tooling() + if (tooling_status == 200) or (tooling_status == 202): + tooling.unpack_tooling(tarball) + else: + LOGGER.info('No optional config tooling provided. status: ' + \ + str(tooling_status)) + LOGGER.debug(str(tooling)) + + LOGGER.debug('Process the Requires and Provides parameters') + + # Process the Requires and Provides parameters until the HTTP status + # from the get_cs_configs and the get_cs_params both return 200 + while config_status or param_status: + + LOGGER.debug('Config Parameter status: ' + str(config_status)) + LOGGER.debug('Return Parameter status: ' + str(param_status)) + + # Get the Required Configs from the Config Server + if config_status: + config_status, configs = cs_client.get_cs_configs() + + # Configure the system with the provided Required Configs + if config_status == 200: + services = parse_require_config(configs) + tooling.invoke_tooling(services) + # don't do any more config status work + # now that the tooling has run + config_status = 0 + else: + LOGGER.info('No configuration parameters provided. status: ' + \ + str(config_status)) + + # Get the requested provides from the Config Server + if param_status: + get_status, params = cs_client.get_cs_params() + + # Gather the values from the system for the requested provides + if get_status == 200: + params_values = generate_provides(params) + else: + params_values = '||' + + # Put the requested provides with values to the Config Server + param_status, body = cs_client.put_cs_params_values(params_values) + if param_status == 200: + # don't operate on params anymore, all have been provided. + param_status = 0 + + # Retry a number of times if 404 HTTP Not Found is returned. + if config_status == 404 or param_status == 404: + LOGGER.error('Requiest to Config Server failed or more to come.') + LOGGER.error('Required Config Parameter status: ' + \ + str(config_status)) + LOGGER.info('Return Parameter status: ' + str(param_status)) + + max_retry -= 1 + if max_retry < 0: + _raise_ASError('Too many erroneous Config Server responses.') + + sleep(10) + +if __name__ == '__main__': + + audrey_script_main() diff --git a/agent/test_audrey_startup.py b/agent/test_audrey_startup.py new file mode 100644 index 0000000..d87c05f --- /dev/null +++ b/agent/test_audrey_startup.py @@ -0,0 +1,692 @@ +#!/usr/bin/python2.6 +''' +* +* Copyright [2011] [Red Hat, Inc.] +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +''' + +''' + test_audrey_agent.py + + Test program for audrey_agent +''' + +import base64 +import logging +import os +import os.path +import tempfile +import unittest +import sys +import tarfile + +from audrey_agent import CSClient +from audrey_agent import ConfigTooling +from audrey_agent import ASError +from audrey_agent import parse_args +from audrey_agent import parse_provides_params +from audrey_agent import parse_require_config +from audrey_agent import audrey_script_main +from audrey_agent import gen_env +from audrey_agent import _run_cmd, _run_pipe_cmd +from audrey_agent import generate_provides +from audrey_agent import setup_logging +from audrey_agent import discover_config_server + +# Helpers and utils +DUMMY_USER_DATA = '1|http://example.com/%7CoauthConsumer%7CoauthSecret' +DUMMY_CS_CONFIG = {'endpoint': 'http://example.com/', + 'oauth_key': 'oauthConsumer', + 'oauth_secret': 'oauthSecret',} + +try: + from cStringIO import StringIO as BIO +except ImportError: # python 3 + from io import BytesIO as BIO + +class HttpUnitTest(object): + ''' + Description: + When testing the http object does not exists. This class provides + test methods that could be preformed when doing UNITTESTing. + ''' + class HttpUnitTestResponse(object): + ''' + Description: + When testing the http object does not exists. This class + provides the test method response that could be preformed + when doing UNITTESTing. + ''' + def __init__(self, status): + self.status = status + + def add_content_disposition(self): + self.__dict__['content-disposition'] = \ + 'attachment; filename=test.tar.gz' + + def __getitem__(self, key): + return self.__dict__[key] + + + # simple HTTP Response with 200 status code + ok_response = HttpUnitTestResponse(200) + not_found_response = HttpUnitTestResponse(404) + + def request(self, url, method='GET', body=None, headers=None): + ''' + Handle request when not running live but in test environment. + ''' + body = '' + response = HttpUnitTest.ok_response + if method == 'GET': + if url.find('/configs/') > -1: + body = '|service|s1|parameters|param1&%s|param2&%s|' % \ + (base64.b64encode('value1'), base64.b64encode('value2')) + elif url.find('/params/') > -1: + body = '|param1¶m2|' + elif url.find('/files/') > -1: + file_out = BIO() + tar = tarfile.open(mode = "w:gz", fileobj = file_out) + tar.add('/etc/passwd') + tar.close() + body = file_out.getvalue() + response.add_content_disposition() + elif url.endswith('/user-data'): + body = base64.b64encode(DUMMY_USER_DATA) + elif url.endswith('/no-version-user-data'): + body = base64.b64encode('0|endpoint') + elif url.endswith('/empty-user-data'): + body = base64.b64encode('') + elif url.endswith('/gimmie-404'): + body = base64.b64encode(DUMMY_USER_DATA) + response = HttpUnitTest.not_found_response + else: + print url + response = HttpUnitTest.not_found_response + #elif method == 'POST' and url.find('/params/') > -1: + # body = '' + return response, body + +def _write_info_file(filepath, cloud): + f = open(filepath, 'w') + f.write(cloud) + f.close() + +# The actual tests + +class TestAudreyStarupRunCmds(unittest.TestCase): + ''' + Test the _run*cmd functions + ''' + def test_success_run_pipe_cmd(self): + self.assertEqual("'test'\n", + _run_pipe_cmd(["echo", "'test'"], ["grep", "test"])['out']) + + def test_cmd2_fail_run_pipe_cmd(self): + self.assertEqual("[Errno 2] No such file or directory", + _run_pipe_cmd(["echo", "'test'"], ["notreal"])['err']) + + def test_cmd1_fail_run_pipe_cmd(self): + self.assertEqual("[Errno 2] No such file or directory", + _run_pipe_cmd(["notreal"], ["echo", "'test'"])['err']) + +class TestAudreyStartupConfigTooling(unittest.TestCase): + ''' + Make sure all the Config tooling is tested + ''' + def test_is_user_supplied(self): + ConfigTooling('test_tooling').is_user_supplied() + + def test_is_rh_supplied(self): + ConfigTooling('test_tooling').is_rh_supplied() + + def test_empty_find_tooling(self): + self.assertRaises(ASError, ConfigTooling('test_tooling').find_tooling, '') + + def test_fail_to_create_tooling_dir(self): + self.assertRaises(ASError, ConfigTooling, tool_dir='/not/real/dir') + +class TestAudreyStartupRequiredConfig(unittest.TestCase): + ''' + Class for exercising the parsing of the Required Configs from the CS. + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + def test_success_service_n_params(self): + ''' + Success case: + - Exercise parse_require_config() with valid input + ''' + # Establish valid test data: + + src = '|service|jon1' + \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon1_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon1_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon1_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_success_service_n_params()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + + # Validate results + self.assertEqual(services[0].name, 'jon1') + self.assertEqual(services[1].name, 'jon2') + + for service in services: + for param in service.params: + name_val = param.split('&') + env_var = 'AUDREY_VAR_' + service.name + '_' + name_val[0] + print 'name_val[0]: ' + str(name_val[0]) + print 'param: ' + str(param) + print 'services.name: ' + str(service.name) + + cmd = ['/usr/bin/printenv', env_var] + ret = _run_cmd(cmd) + self.assertEqual(ret['out'][:-1], \ + validation_dict[env_var]) + + def test_success_empty_source(self): + ''' + Success case: + - Exercise parse_require_config() with valid empty input + ''' + + # Establish valid test data: + src = '||' + print '\nTest Name: test_success_empty_source()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + print 'services: ' + str(services) + + # Validate results + self.assertEqual(services, []) + + def test_success_empty_service(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + ''' + + # Establish valid test data: + src = '|service|' + \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_success_empty_service()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + + # Validate results + self.assertEqual(services[0].name, '') + self.assertEqual(services[1].name, 'jon2') + + for service in services: + for param in service.params: + name_val = param.split('&') + env_var = 'AUDREY_VAR_' + name_val[0] + + print 'name_val[0]: ' + str(name_val[0]) + print 'param: ' + str(param) + print 'services.name: ' + str(service.name) + + cmd = ['/usr/bin/printenv', env_var] + ret = _run_cmd(cmd) + self.assertEqual(ret['out'][:-1], \ + validation_dict[env_var]) + + def test_failure_no_services_name(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + + The slight difference between this test and test_success_empty_services + is the success case has an empty service name indicated by "||": + |service||paramseters + + and the failure case has no service name: + |service|paramseters + + ''' + + # Establish valid test data: + src = '|service' \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_failure_no_service_names()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + with self.assertRaises(ASError): + print 'parse_require_config returned: ' + \ + str(parse_require_config(src)) + + def test_failure_bad_service_name(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + ''' + + # Establish valid test data: + src = '|service|parameters|' + print '\nTest Name: test_failure_bad_service_name()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() ASError' + + # Exersise code segment + with self.assertRaises(ASError): + print 'parse_require_config returned: ' + \ + str(parse_require_config(src)) + +class TestAudreyStartupDiscovery(unittest.TestCase): + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, 'test_audrey_agent.log') + self.cloud_info_file = 'cloud_info' + self.condor_addr_file = 'condor_addr' + self.condor_uuid_file = 'condor_uuid' + + def tearDown(self): + os.remove(self.cloud_info_file) + if os.path.exists(self.condor_addr_file): + os.remove(self.condor_addr_file) + if os.path.exists(self.condor_uuid_file): + os.remove(self.condor_uuid_file) + + def test_ec2(self): + _write_info_file(self.cloud_info_file, 'EC2') + discover_config_server(self.cloud_info_file, http=HttpUnitTest()) + + def test_ec2_404(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/gimmie-404') + + def test_condorcloud(self): + _write_info_file(self.condor_addr_file, '1|endpoint|secret') + _write_info_file(self.condor_uuid_file, 'key') + _write_info_file(self.cloud_info_file, 'CONDORCLOUD') + discover_config_server(self.cloud_info_file, + condor_addr_file=self.condor_addr_file, + condor_uuid_file=self.condor_uuid_file) + + def test_rhev(self): + _write_info_file(self.cloud_info_file, 'RHEV') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file) + + def test_vsphere(self): + _write_info_file(self.cloud_info_file, 'VSPHERE') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file) + + def test_invalid_user_data_version(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/no-version-user-data') + + def test_invalid_user_data_no_delim(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/empty-user-data') + + +class TestAudreyStartupProvidesParameters(unittest.TestCase): + ''' + Class for exercising the parsing of the Provides ParametersConfigs + from the CS. + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + def test_success_parameters(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + ''' + + # Establish valid test data: + src = '|operatingsystem&is_virtual|' + + print '\nTest Name: test_success_parameters()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['operatingsystem', 'is_virtual'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + print 'len(provides): ' + str(len(provides)) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + self.assertTrue('audrey_data=%7Coperatingsystem' in provides) + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + def test_success_no_params(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - Containging an unavailable parameter + ''' + + # Establish valid test data: + src = '|uptime_days&unavailable_dogs&ipaddress|' + + print '\nTest Name: test_success_no_params()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['uptime_days', 'unavailable_dogs', 'ipaddress'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + # Confirm unavailable parameters return an empty string. + self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) + + def test_success_one_parameters(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - with only one parameter + ''' + + # Establish valid test data: + src = '|uptime_days|' + + print '\nTest Name: test_success_parameters()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['uptime_days'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + def test_success_one_parameter(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - With only one parameter which is unavailable + ''' + + # Establish valid test data: + src = '|unavailable_dogs|' + + print '\nTest Name: test_success_one_parameter()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['unavailable_dogs'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + # Confirm unavailable parameters return an empty string. + self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) + + def test_failure_missing_delimiter(self): + ''' + Failure case: + - Exercise parse_provides_params() and generate_provides() + with invalid input + - missing leading delimiter + ''' + + # Establish valid test data: + src = 'unavailable_dogs|' + + print '\nTest Name: test_failure_missing_delimiter()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() ASError' + + expected_params_list = ['unavailable_dogs'] + + # Exersise code segment and validate results + with self.assertRaises(ASError): + params_list = parse_provides_params(src) + + with self.assertRaises(ASError): + provides = generate_provides(src) + +class TestConfigServerClient(unittest.TestCase): + ''' + Class for exercising the gets and put to and from the CS + ''' + + def setUp(self): + ''' + If the cloud info file is not present assume running in a + UNITTEST environment. This will allow for exercising some + of the code without having to be running in a cloud VM. + + Set up logging. + ''' + + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + # Create the client Object + self.cs_client = CSClient(**DUMMY_CS_CONFIG) + self.cs_client.http = HttpUnitTest() + + def tearDown(self): + pass + + def test_success_get_cs_configs(self): + ''' + Success case: + - Exercise get_cs_configs() + ''' + print '\n\n--- Test Name: test_success_get_cs_configs ---' + + self.cs_client.get_cs_configs() + + # Add asserts + print 'test_success_get_cs_configs() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + + def test_success_get_cs_tooling(self): + ''' + Success case: + - Exercise get_cs_tooling() + ''' + self.cs_client.get_cs_tooling() + + def test_success_get_cs_params(self): + ''' + Success case: + - Exercise get_cs_params() + ''' + print '\n\n--- Test Name: test_success_get_cs_params ---' + + self.cs_client.get_cs_params() + + # Add asserts + print 'test_success_get_cs_params() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + + def test_success_get_cs_confs_n_params(self): + ''' + Success case: + - Exercise get_cs_configs() and get_cs_params() + ''' + print '\n\n--- Test Name: test_success_get_cs_confs_and_params ---' + + self.cs_client.get_cs_configs() + self.cs_client.get_cs_params() + + # Add asserts + print 'test_success_get_cs_confs_n_params() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + print 'test_success_get_cs_confs_n_params() Add asserts' + + def test_success_put_cs_params_values(self): + ''' + Success case: + - Exercise put_cs_params_values() + ''' + self.cs_client.put_cs_params_values('') + + def test_error_http_status(self): + ''' + Success case: + - Exercise put_cs_params_values() + ''' + self.assertRaises(ASError, self.cs_client._validate_http_status, 401) + +class TestAudreyScript(unittest.TestCase): + ''' + Class for exercising the full audrey script functionality + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + + This test currently require to be run in a cloud VM + with a live Config Server. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + # make a copy of argv + self.argv = list(sys.argv) + + def tearDown(self): + # replace argv + sys.argv = list(self.argv) + + def test_audrey_script_main(self): + ''' + Perform what the audrey script will do. + ''' + cloud_info_file = 'cloud_info' + sys.argv.extend(['-p']) + _write_info_file(cloud_info_file, 'EC2') + audrey_script_main(HttpUnitTest()) + os.remove(cloud_info_file) + + def test_fail_audrey_script_main(self): + ''' + Perform what the audrey script will do. + ''' + self.assertRaises(ASError, audrey_script_main) + + def test_empty_gen_env(self): + self.assertRaises(ASError, gen_env, '', '') + + # doesn't actually test what I wanted it to. + #def test_parse_require_config(self): + # self.assertRaises(ASError, parse_require_config, '') + +if __name__ == '__main__': + + setup_logging(logging.DEBUG, logfile_name='./test_audrey_agent.log') + unittest.main() diff --git a/audrey_start/COPYING b/audrey_start/COPYING deleted file mode 100644 index 35907a1..0000000 --- a/audrey_start/COPYING +++ /dev/null @@ -1,158 +0,0 @@ -Apache License - -Version 2.0, January 2004 - -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, -and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the -copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other -entities that control, are controlled by, or are under common control with -that entity. For the purposes of this definition, "control" means (i) the -power, direct or indirect, to cause the direction or management of such -entity, whether by contract or otherwise, or (ii) ownership of fifty percent -(50%) or more of the outstanding shares, or (iii) beneficial ownership -of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions -granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation -or translation of a Source form, including but not limited to compiled -object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, -made available under the License, as indicated by a copyright notice that -is included in or attached to the work (an example is provided in the Appendix -below). - -"Derivative Works" shall mean any work, whether in Source or Object form, -that is based on (or derived from) the Work and for which the editorial -revisions, annotations, elaborations, or other modifications represent, -as a whole, an original work of authorship. For the purposes of this License, -Derivative Works shall not include works that remain separable from, or -merely link (or bind by name) to the interfaces of, the Work and Derivative -Works thereof. - -"Contribution" shall mean any work of authorship, including the original -version of the Work and any modifications or additions to that Work or -Derivative Works thereof, that is intentionally submitted to Licensor for -inclusion in the Work by the copyright owner or by an individual or Legal -Entity authorized to submit on behalf of the copyright owner. For the purposes -of this definition, "submitted" means any form of electronic, verbal, or -written communication sent to the Licensor or its representatives, including -but not limited to communication on electronic mailing lists, source code -control systems, and issue tracking systems that are managed by, or on -behalf of, the Licensor for the purpose of discussing and improving the -Work, but excluding communication that is conspicuously marked or otherwise -designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on -behalf of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this -License, each Contributor hereby grants to You a perpetual, worldwide, -non-exclusive, no-charge, royalty-free, irrevocable copyright license to -reproduce, prepare Derivative Works of, publicly display, publicly perform, -sublicense, and distribute the Work and such Derivative Works in Source -or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this -License, each Contributor hereby grants to You a perpetual, worldwide, -non-exclusive, no-charge, royalty-free, irrevocable (except as stated in -this section) patent license to make, have made, use, offer to sell, sell, -import, and otherwise transfer the Work, where such license applies only -to those patent claims licensable by such Contributor that are necessarily -infringed by their Contribution(s) alone or by combination of their Contribution(s) -with the Work to which such Contribution(s) was submitted. If You institute -patent litigation against any entity (including a cross-claim or counterclaim -in a lawsuit) alleging that the Work or a Contribution incorporated within -the Work constitutes direct or contributory patent infringement, then any -patent licenses granted to You under this License for that Work shall terminate -as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work -or Derivative Works thereof in any medium, with or without modifications, and -in Source or Object form, provided that You meet the following conditions: - -1. You must give any other recipients of the Work or Derivative Works -a copy of this License; and -2. You must cause any modified files to carry prominent notices stating -that You changed the files; and -3. You must retain, in the Source form of any Derivative Works that -You distribute, all copyright, patent, trademark, and attribution notices -from the Source form of the Work, excluding those notices that do not pertain -to any part of the Derivative Works; and -4. If the Work includes a "NOTICE" text file as part of its distribution, -then any Derivative Works that You distribute must include a readable copy -of the attribution notices contained within such NOTICE file, excluding -those notices that do not pertain to any part of the Derivative Works, -in at least one of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or documentation, -if provided along with the Derivative Works; or, within a display generated -by the Derivative Works, if and wherever such third-party notices normally -appear. The contents of the NOTICE file are for informational purposes -only and do not modify the License. You may add Your own attribution notices -within Derivative Works that You distribute, alongside or as an addendum -to the NOTICE text from the Work, provided that such additional attribution -notices cannot be construed as modifying the License. You may add Your -own copyright statement to Your modifications and may provide additional -or different license terms and conditions for use, reproduction, or distribution -of Your modifications, or for any such Derivative Works as a whole, provided -Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, -any Contribution intentionally submitted for inclusion in the Work by You -to the Licensor shall be under the terms and conditions of this License, -without any additional terms or conditions. Notwithstanding the above, -nothing herein shall supersede or modify the terms of any separate license -agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade -names, trademarks, service marks, or product names of the Licensor, except -as required for reasonable and customary use in describing the origin of -the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable -law or agreed to in writing, Licensor provides the Work (and each Contributor -provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied, including, without limitation, -any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, -or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining -the appropriateness of using or redistributing the Work and assume any -risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether -in tort (including negligence), contract, or otherwise, unless required -by applicable law (such as deliberate and grossly negligent acts) or agreed -to in writing, shall any Contributor be liable to You for damages, including -any direct, indirect, special, incidental, or consequential damages of -any character arising as a result of this License or out of the use or -inability to use the Work (including but not limited to damages for loss -of goodwill, work stoppage, computer failure or malfunction, or any and -all other commercial damages or losses), even if such Contributor has been -advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the -Work or Derivative Works thereof, You may choose to offer, and charge a -fee for, acceptance of support, warranty, indemnity, or other liability -obligations and/or rights consistent with this License. However, in accepting -such obligations, You may act only on Your own behalf and on Your sole -responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any -liability incurred by, or claims asserted against, such Contributor by -reason of your accepting any such warranty or additional liability. diff --git a/audrey_start/Makefile b/audrey_start/Makefile deleted file mode 100644 index 8cd1bf5..0000000 --- a/audrey_start/Makefile +++ /dev/null @@ -1,94 +0,0 @@ -# -# Copyright [2011] [Red Hat, Inc.] -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -AUDREY_STARTUP_CACHE_DIR ?= $(HOME)/audrey-agent-cache - -VERSION = 0.4.2 - -# For Release: 0..., set _audrey-agent_dev=1 so that we get extra_release.GIT- -# annotated rpm version strings. -_audrey-agent_dev = $(shell grep -q '^[[:space:]]*Release:[[:space:]]*0' \ - aeolus-audrey-agent.spec.in && echo 1 || :) - -# use $(shell...) here to collect the git head and date *once* per make target. -# that ensures that if multiple actions happen in the same target (like the -# multiple RPM builds in the rpms target), they all use the same date -git_head = $(shell git log -1 --pretty=format:%h) -date = $(shell date --utc +%Y%m%d%H%M%S) -GIT_RELEASE = $(date)git$(git_head) -RPMDIR = $$(rpm --eval '%{_rpmdir}') -RELEASE = $(shell grep ^Release: aeolus-audrey-agent.spec.in | \ - sed -e 's/Release:[ , ]*//' | sed -e 's/%.*//') -# RPM_FLAGS = --define "audrey-agent_cache_dir $(AUDREY_STARTUP_CACHE_DIR)" -# RPM_FLAGS += $(if $(_audrey-agent_dev),--define "extra_release .$(GIT_RELEASE)") -man_section = 8 - -# Create the dist. -# Run the audrey_startup.py audrey.$(man_section) then: -# Set the version numbers in aeolus-audrey-agent.spec and audrey_startup.py -# Copy the desired bits to the dist directory. -# tar up the contents of the dist directory. -dist: audrey_startup.py audrey.$(man_section) - rm -rf dist - sed -e 's/@VERSION@/$(VERSION)/' aeolus-audrey-agent.spec.in \ - > aeolus-audrey-agent.spec - mkdir -p dist/aeolus-audrey-agent-$(VERSION) - cp -a aeolus-audrey-agent.spec aeolus-audrey-agent.spec.in \ - COPYING Makefile audrey_startup.in.py \ - audrey.$(man_section) \ - test_audrey_startup.py dist/aeolus-audrey-agent-$(VERSION) - tar -C dist -zcvf aeolus-audrey-agent-$(VERSION).tar.gz aeolus-audrey-agent-$(VERSION) - -# cp audrey_starup.in.py to audrey_startup.py -audrey_startup.py: audrey_startup.in.py - sed -e 's/@VERSION@/$(VERSION)/' $< > $@-t - chmod a+x,a-w $@-t - mv $@-t $@ - -# cp audrey_starup.py to audrey. -audrey: audrey_startup.py - cp $< $@ - -# Generate the man page using help2man from the --help text output from the Audrey agent. -# Run the audrey target before geneeerating the man page. -audrey.$(man_section): audrey - help2man --name='Aeolus startup agent' --section $(man_section) \ - --no-info ./$< > $@-t - mv $@-t $@ - -# Run the automated tests -# Run the audrey_startup.py target before running the automated tests. -check: audrey_startup.py - python test_audrey_startup.py - -# Build the rpms -# Run the check target followed by the dist target before building the rpms. -rpms: check dist - rpmbuild -ta aeolus-audrey-agent-$(VERSION).tar.gz - -# Build the srpms -srpms: check dist - rpmbuild -ts aeolus-audrey-agent-$(VERSION).tar.gz - -# Clean up files generated by this Makefile -clean: - rm -rf dist aeolus-audrey-agent-$(VERSION).tar.gz \ - aeolus-audrey-agent.spec audrey.$(man_section) audrey \ - audrey.log audrey_startup.py audrey_startup.pyc \ - test_toolinglog test_toolinguser \ - toolinglog toolinguser - -.PHONY: dist check rpms srpms diff --git a/audrey_start/aeolus-audrey-agent.spec.in b/audrey_start/aeolus-audrey-agent.spec.in deleted file mode 100644 index a43d39d..0000000 --- a/audrey_start/aeolus-audrey-agent.spec.in +++ /dev/null @@ -1,100 +0,0 @@ -# -# Copyright [2011] [Red Hat, Inc.] -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -Name: aeolus-audrey-agent -Version: @VERSION@ -Release: 12%{?dist} -Summary: The Aeolus Audrey Startup Agent - -Group: Applications/System -License: ASL 2.0 -URL: http://aeolusproject.org -Source0: http://joev.fedorapeople.org/audrey-agent/aeolus-audrey-agent-%%7Bversion%7D... - -# All of these are required for building since during the build -# process "audrey --help" is executed to generate the man page. -BuildRequires: help2man -BuildRequires: facter python-httplib2 python-oauth2 -%if (0%{?fedora} <= 14 || 0%{?rhel} <= 6) -BuildRequires: python-argparse -%endif - -Requires: facter python-httplib2 python-oauth2 -Requires: python-argparse -%if (0%{?fedora} <= 14 || 0%{?rhel} <= 6) -Requires: python-argparse -%endif - -BuildArch: noarch - -%description -The Aeolus Audrey Startup Agent, a script which runs on a booting -cloud instance to retrieve configuration data from the Aeolus -Config Server. - -%prep -%setup -q - -%build -make %{?_smp_mflags} - -%install -rm -rf %{buildroot} -mkdir -p %{buildroot}%{_bindir} -mkdir -p %{buildroot}%{_mandir}/man8 - -# copy over the audrey startup agent bits -install audrey_startup.py %{buildroot}%{_bindir}/audrey -cp audrey.8 %{buildroot}%{_mandir}/man8 - -%files -%{_bindir}/audrey -%{_mandir}/man8/audrey.8* -%doc COPYING - -%changelog -* Thu Nov 18 2011 Joe VLcek jvlcek@redhat.com 0.4.0-12 -- Fixed Auto help file generation induced build failure -* Thu Nov 17 2011 Joe VLcek jvlcek@redhat.com 0.4.0-11 -- Remove Auto help file generation as it is currently breakin the build -* Thu Nov 17 2011 Joe VLcek jvlcek@redhat.com 0.4.0-10 -- Fix bz754769 fix user data parsing and log level -* Tue Nov 09 2011 Joe VLcek jvlcek@redhat.com 0.4.0-9 -- Update the licensing information -* Tue Nov 08 2011 Joe VLcek jvlcek@redhat.com 0.4.0-8 -- Add man page generation and address some packaging review feedback -* Tue Nov 08 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-7 -- Fixing changelog history -* Mon Nov 07 2011 Joe VLcek jvlcek@redhat.com 0.3.1-7 -- Address Packaging for Fedora review feedback -* Fri Nov 05 2011 Joe VLcek jvlcek@redhat.com 0.3.1-6 -- Packaging for Fedora -* Wed Nov 02 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-6 -- Handles base64 encoded and decoded data in user data -* Wed Nov 02 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-3 -- Fix for audrey.log location -* Thu Oct 27 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-2 -- Radez doubled unit test coverage -* Wed Oct 26 2011 Greg Blomquist gblomqui@redhat.com 0.4.0-1 -- Now using oauth to authenticate with Config Server -- Parsing a new user data format that is versioned -* Thu Oct 20 2011 Dan Radez dradez@redhat.com 0.3.1-3 -- adding requires for rpm installations -* Tue Oct 11 2011 Joe VLcek joev@redhat.com 0.3.1-2 -- RHEVm user data injection base64 encoded. -* Fri Sep 30 2011 Joe VLcek joev@redhat.com 0.3.1-1 -- RHEVm user data injection. -* Wed May 18 2011 Joe VLcek joev@redhat.com 0.0.1-1 -- Initial build. diff --git a/audrey_start/audrey_startup.in.py b/audrey_start/audrey_startup.in.py deleted file mode 100755 index eb9d26f..0000000 --- a/audrey_start/audrey_startup.in.py +++ /dev/null @@ -1,1353 +0,0 @@ -#! /usr/bin/env python -''' -* -* Copyright [2011] [Red Hat, Inc.] -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -''' - -''' -Audrey Startup (AS) - -Note: The source file is named audrey_start.in.py The make process generates - audrey_start.py. audrey_start.py should not be manually modified. - -Invoked at instance launch to interface with the Config Server (CS) - -For prototype end to end testing this file needs to be installed -at: /usr/bin/audrey - -Algorithim: - Get CF info - Loop: - Get and validate required configuration from CS - Configure system using required configuration - Get and validate provides parameters from CS - Gather provided parameter data from system - Put provided parameter data to CS - if not done then goto Loop - -''' - -import argparse -import base64 -import httplib2 -import logging -import os -import shutil -import sys -import tarfile -import tarfile as tf # To simplify exception names. -import tempfile -import urllib -import oauth2 as oauth - -from time import sleep -from collections import deque -from subprocess import Popen, PIPE - -EC2_USER_DATA_URL = 'http://169.254.169.254/latest/user-data' -CLOUD_INFO_FILE = '/etc/sysconfig/cloud-info' - -# Location of the config tooling. -TOOLING_DIR = '/var/audrey/tooling/' - -# Log file -LOG = '/var/log/audrey.log' -LOGGER = None -CS_API_VER = 1 -# The VERSION string is filled in during the make process. -AUDREY_VER = '@VERSION@' - -# When running on condor-cloud, the Config Server (CS) contact -# information will be stored in the smbios. -# These are the dmi files where the smbios information is stored. -CONDORCLOUD_CS_ADDR = '/sys/devices/virtual/dmi/id/sys_vendor' -CONDORCLOUD_CS_UUID = '/sys/devices/virtual/dmi/id/product_name' - -# -# Error Handling methods: -# -class ASError(Exception): - ''' - Some sort of error occurred. The exact cause of the error should - have been logged. So, this just indicates that something is wrong. - ''' - pass - -def _raise_ASError(err_msg): - ''' - Log an error message and raise ASError - ''' - LOGGER.error(err_msg) - raise ASError(err_msg) - -class _run_cmd_return_subproc(): - ''' - Used to pass return code to caller if no subprocess object - is generated by Popen() due to an error. - ''' - returncode = 127 - -# -# Misc. Supporting Methods -# -def _run_cmd(cmd, my_cwd=None): - ''' - Description: - Run a command given by a dictionary, check for stderr output, - return code. - - To check the return code catch SystemExit then examine: - ret['subproc'].returncode. - - Input: - - cmd - a list containing the command to execute. - e.g.: cmd = ['ls', '/tmp'] - - Returns: - - ret - a dictionary upon return contains the keys: - 'subproc', 'err', 'out' - - ret['subproc'].returncode = subprocess return code - ret['err'] = command errors string. - ret['out'] = command out list. - - Example: - - cmd = ['ls', '/tmp'] - ret = _run_cmd(cmd) - - ret.keys() - ['subproc', 'err', 'out'] - ret['subproc'].returncode - 0 - ret['err'] - '' - ret['out'] - - ''' - - pfail = _run_cmd_return_subproc() - - # Return dictionary to contain keys: 'cmd', 'subproc', 'err', 'out' - ret = {'subproc' : None, 'err' : '' , 'out' : ''} - - try: - ret['subproc'] = Popen(cmd, cwd=my_cwd, stdout=PIPE, stderr=PIPE) - - # unable to find command will result in an OSError - except OSError, err: - if not ret['subproc']: - ret['subproc'] = pfail - - ret['subproc'].returncode = 127 # command not found - ret['err'] = str(err) - return ret - - # fill ret['out'] with stdout and ret['err'] with stderr - ret.update(zip(['out', 'err'], ret['subproc'].communicate())) - - return ret - -def _run_pipe_cmd(cmd1, cmd2): - ''' - Description: - Run one command piped into another. Commands are given as - dictionaries, check for stderr output, return code. - - To check the return code catch SystemExit then examine: - ret['subproc'].returncode. - - That is this routine can be used to execute a command - of the form: - - Input: - - cmd1 - a list containing the command to execute. - e.g.: cmd = ['ls', '/tmp'] - - cmd2 - a list containing the command to pipe the output - of cmd1 to. - e.g.: cmd = ['grep', 'a_file'] - - Returns: - - ret - a dictionary upon return contains the keys: - 'subproc', 'err', 'out' - - ret['subproc'].returncode = subprocess return code - ret['err'] = command errors string. - ret['out'] = command out list. - - Example: - - cmd1 = ['ls', '/tmp'] - cmd2 = ['grep', 'a_file'] - ret = _run_pipe_cmd(cmd1, cmd2) - - ret.keys() - ['subproc', 'err', 'out'] - ret['subproc'].returncode - 0 - ret['err'] - '' - ret['out'] - - ''' - - # Return dictionary to contain keys: 'cmd', 'subproc', 'err', 'out' - ret = {'subproc' : None, 'err' : '' , 'out' : ''} - - p1 = None - p2 = None - pfail = _run_cmd_return_subproc() - - # Execute the first command: - try: - p1 = Popen(cmd1, stdout=PIPE) - p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE ) - p1.stdout.close() - - # fill ret['out'] with stdout and ret['err'] with stderr - # ret.update(zip(['out', 'err'], ret['subproc'].communicate()[0])) - ret.update(zip(['out', 'err'], p2.communicate())) - ret['subproc'] = p2 - - # unable to find command will result in an OSError - except OSError, err: - if p2: - ret['subproc'] = p2 - elif p1: - ret['subproc'] = p1 - else: - ret['subproc'] = pfail - - ret['subproc'].returncode = 127 # command not found - ret['err'] = str(err) - return ret - - return ret - -class ServiceParams(object): - ''' - Description: - Used for storing a service and all of it's associated parameters - as provided by the Config Server in the "required" parameters - API message. - - services = [ - ServiceParams('serviceA', ['n&v', 'n&v', 'n&v',...]), - ServiceParams('serviceB', ['n&v', 'n&v', 'n&v',...]), - ServiceParams('serviceB', ['n&v', 'n&v', 'n&v',...]), - ] - - This structure aids in tracking the parsed required config - parameters which is useful when doing UNITTESTing. - - ''' - def __init__(self, name=None): - if name == None: - name = '' - self.name = name # string - self.params = [] # start with an empty list - def add_param(self, param): - ''' - Description: - Add a parameter provided by the Config Server to the list. - ''' - self.params.append(param) - def __repr__(self): - return repr((self.name, self.params)) - -# -# Methods used to parse the CS<->AS text based API -# -def _common_validate_message(src): - ''' - Perform validation of the text message sent from the Config Server. - ''' - - if not src.startswith('|') or not src.endswith('|'): - _raise_ASError(('Invalid start and end characters: %s') % (src)) - -def gen_env(serv_name, param_val): - ''' - Description: - Generate the os environment variables from the required config string. - - Input: - serv_name - A service name - e.g.: - jon_agent_config - - param_val - A parameter name&val pair. The value is base64 encoded. - e.g.: - jon_server_ip&MTkyLjE2OC4wLjE= - - Output: - Set environment variables of the form: - <name>=<value> - e.g.: - jon_server_ip=base64.b64decode('MTkyLjE2OC4wLjE=') - jon_server_ip='192.168.0.1 - - Raises ASError when encountering an error. - - ''' - LOGGER.debug('Invoked gen_env()') - - # If the param_val is missing treat as an exception. - if param_val == '': - _raise_ASError(('Missing parameter name. %s') % \ - (str(param_val))) - - # If serv_name is not blank an extra "_" must be added to - # the environment variable name. - if serv_name != '': - serv_name = serv_name + '_' - - name_val = param_val.split('&') - var_name = 'AUDREY_VAR_' + serv_name + name_val[0] - os.environ[var_name] = \ - base64.b64decode(name_val[1]) - - # Get what was set and log it. - cmd = ['/usr/bin/printenv', var_name] - ret = _run_cmd(cmd) - LOGGER.debug(var_name + '=' + str(ret['out'].strip())) - -def parse_require_config(src): - ''' - Description: - Parse the required config text message sent from the Config Server. - - Input: - The required config string obtained from the Config Server, - delimited by an | and an & - - Two tags will mark the sections of the data, - '|service|' and '|parameters|' - - To ensure all the data was received the entire string will be - terminated with an "|". - - The string "|service|" will precede a service names. - - The string "|parameters|" will precede the parameters for - the preceeding service, in the form: names&<b64 encoded values>. - - This will be a continuous text string (no CR or New Line). - - Format (repeating for each service): - - |service|<s1>|parameters|name1&<b64val>|name2&<b64val>...|nameN&<b64v>| - - - e.g.: - |service|ssh::server|parameters|ssh_port&<b64('22')> - |service|apache2::common|apache_port&<b64('8081')>| - - Returns: - - A list of ServiceParams objects. - ''' - - services = [] - new = None - - _common_validate_message(src) - - # Message specific validation - if src == '||': - # special case indicating no required config needed. - return [] - - if src.find('|service|') != 0: - _raise_ASError(('|service| is not the first tag found. %s') % (src)) - - - src_q = deque(src.split('|')) - - # remove leading and trailing elements from the src_q since they are - # empty strings generated by the split('|') because of the leading - # and trailing '|' - token = src_q.popleft() - token = src_q.pop() - - while True: - try: - token = src_q.popleft() - if token == 'service': - token = src_q.popleft() # next token is service name - - # Raise an error if the service name is invalid. - if token.find('&') != -1 or \ - token == 'service' or \ - token == 'parameters': - _raise_ASError(('ERROR invalid service name: %s') % \ - (str(token))) - - new = ServiceParams(token) - services.append(new) - elif token == 'parameters' or token == '': - pass - else: # token is a name&value pair. - if token.find('&') == -1: - _raise_ASError(('ERROR name&val: %s missing delimiter') % \ - (str(token))) - if new: - new.add_param(token) - gen_env(new.name, token) - else: - _raise_ASError(('ERROR missing service tag %s') % \ - (str(src))) - except IndexError: - break - - return services - -def _get_system_info(): - ''' - Description: - Get the system info to be used for generating this instances - provides back to the Config Server. - - Currently utilizes Puppet's facter via a Python subprocess call. - - Input: - None - - Returns: - A dictionary of system info name/value pairs. - - ''' - - cmd = ['/usr/bin/facter'] - ret = _run_cmd(cmd) - if ret['subproc'].returncode != 0: - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - facts = {} - for fact in ret['out'].split('\n'): - if fact: # Handle the new line at the end of the facter output - name, val = fact.split(' => ') - facts[ name ] = val.rstrip() - - return facts - -def parse_provides_params(src): - ''' - Description: - Parse the provides parameters text message sent from the - Config Server. - - Input: - The provides parameters string obtained from the Config Server. - - The delimiters will be an | and an & - - To ensure all the data was received the entire string will be - terminated with an "|". - - This will be a continuous text string (no CR or New Line). - - Format: - |name1&name2...&nameN| - - e.g.: - |ipaddress&virtual| - - Returns: - - a list of parameter names. - ''' - - _common_validate_message(src) - - # Message specific validation - if src == '||': - # special case indicating no provides parameters requested. - return [''] - - params_str = src[src.find('|')+1:len(src)-1] - - return params_str.split('&') - -def generate_provides(src): - ''' - Description: - Generate the provides parameters list. - Uses parse_provides_params() - - Input: - The provides parameters string obtained from the Config Server. - - Returns: - A string to send back to the Config Server with prifix - 'audrey_data='<url encoded return data>' - - The return portion will be delimited with an | and an & - - To ensure all the data is transmitted the entire string will be - terminated with an "|". - - This will be a continuous text string (no CR or New Line). - - Data portion Format: - |name1&val1|name2&val...|nameN$valN| - - e.g.: - |ipaddress&<b64/10.118.46.205>|virtual&<b64/xenu>| - - The return string format: - "audrey_data=<url encoded data portion>" - - - ''' - LOGGER.info('Invoked generate_provides()') - - provides_dict = {} - params_list = parse_provides_params(src) - - system_info_dict = _get_system_info() - - for param in params_list: - try: - provides_dict.update( \ - {param:base64.b64encode(system_info_dict[param])}) - except KeyError: - # A specified parameter is not found. Provide value '' - provides_dict.update({param:''}) - - - # Create string to send to Config Server - provides_list = [''] - for key in provides_dict.keys(): - provides_list.append(str(key) + '&' + str(provides_dict[key])) - provides_list.append('') - - return urllib.urlencode({'audrey_data':'|'.join(provides_list)}) - -class ConfigTooling(object): - ''' - TBD - Consider making this class derived from dictionary or a mutable - mapping. - - Description: - Interface to configuration tooling: - - Getting optional user supplied tooling from CS - - Verify and Unpack optional user supplied tooling retrieved - from CS - - Is tooling for a given service user supplied - - Is tooling for a given service Red Hat supplied - - Find tooling for a given service Red Hat supplied - - List tooling for services and indicate if it is user or Red - Hat supplied. - ''' - - def __init__(self, tool_dir=TOOLING_DIR): - ''' - Description: - Set initial state so it can be tracked. Valuable for - testing and debugging. - ''' - self.tool_dir = tool_dir - self.user_dir = tool_dir + 'user/' - self.log = tool_dir + 'log' - self.tarball = '' - - # Create the extraction destination - try: - os.makedirs(self.user_dir) - except OSError, (errno, strerror): - if errno is 17: # File exists - pass - else: - _raise_ASError(('Failed to create directory %s. ' + \ - 'Error: %s') % (self.user_dir, strerror)) - - self.ct_logger = logging.getLogger('ConfigTooling') - self.ct_logger.addHandler(logging.FileHandler(self.log)) - - def __str__(self): - ''' - Description: - Called by the str() function and by the print statement to - produce the informal string representation of an object. - ''' - return('\n<Instance of: %s\n' \ - '\tTooling Dir: %s\n' \ - '\tUnpack User Tooling Tarball Dir: %s\n' \ - '\tLog File: %s\n' \ - '\ttarball Name: %s\n' \ - 'eot>' % - (self.__class__.__name__, - str(self.tool_dir), - str(self.user_dir), - str(self.log), - str(self.tarball), - )) - - def log_info(self, log_str): - ''' - Description: - Used for logging the commands that have been executed - along with their output and return codes. - - Simply logs the provided input string. - ''' - self.ct_logger.info(log_str) - - def log_error(self, log_str): - ''' - Description: - Used for logging errors encountered when attempting to - execute the service command. - - Simply logs the provided input string. - ''' - self.ct_logger.error(log_str) - - def invoke_tooling(self, services): - ''' - Description: - Invoke the configuration tooling for the specified services. - - Input: - services - A list of ServiceParams objects. - - ''' - - # For now invoke them all. Later versions will invoke the service - # based on the required params from the Config Server. - LOGGER.debug('Invoked ConfigTooling.invoke_tooling()') - LOGGER.debug(str(services)) - for service in services: - - try: - top_level, tooling_path = self.find_tooling(service.name) - except ASError: - # No tooling found. Try the next service. - continue - - cmd = [tooling_path] - cmd_dir = os.path.dirname(tooling_path) - ret = _run_cmd(cmd, cmd_dir) - self.log_info('Execute Tooling command: ' + ' '.join(cmd)) - - retcode = ret['subproc'].returncode - if retcode == 0: - # Command successed, log the output. - self.log_info('return code: ' + str(retcode)) - self.log_info('\n\tStart Output of: ' + ' '.join(cmd) + \ - ' >>>\n' + \ - str(ret['out']) + \ - '\n\t<<< End Output') - else: - # Command failed, log the errors. - self.log_info('\n\tStart Output of: ' + ' '.join(cmd) + \ - ' >>>\n' + \ - str(ret['out']) + \ - '\n\t<<< End Output') - self.log_error('error code: ' + str(retcode)) - self.log_error('error msg: ' + str(ret['err'])) - - # If tooling was provided at the top level only run it once - # for all services listed in the required config params. - if top_level: - break - - def unpack_tooling(self, tarball): - ''' - Description: - Methods used to untar the user provided tarball - - Perform validation of the text message sent from the - Config Server. Validate, open and write out the contents - of the user provided tarball. - ''' - LOGGER.info('Invoked unpack_tooling()') - LOGGER.debug('tarball: ' + str(tarball) + \ - 'Target Direcory: ' + str(self.user_dir)) - - self.tarball = tarball - - # Validate the specified tarfile. - try: - if not tarfile.is_tarfile(self.tarball): - # If file exists but is not a tar file force IOError. - raise IOError - except IOError, (errno, strerror): - _raise_ASError(('File was not found or is not a tar file: %s ' + \ - 'Error: %s %s') % (self.tarball, errno, strerror)) - - # Attempt to extract the contents from the specified tarfile. - # - # If tarfile access or content is bad report to the user to aid - # problem resolution. - try: - tarf = tarfile.open(self.tarball) - tarf.extractall(path=self.user_dir) - tarf.close() - except IOError, (errno, strerror): - _raise_ASError(('Failed to access tar file %s. Error: %s') % \ - (self.tarball, strerror)) - # Capture and report errors with the tarfile - except (tf.TarError, tf.ReadError, tf.CompressionError, \ - tf.StreamError, tf.ExtractError), (strerror): - - _raise_ASError(('Failed to access tar file %s. Error: %s') % \ - (self.tarball, strerror)) - - def is_user_supplied(self): - ''' - Description: - Is the the configuration tooling for the specified service - supplied by the user? - - TBD: Take in a service_name and evaluate. - def is_user_supplied(self, service_name): - ''' - return True - - def is_rh_supplied(self): - ''' - Description: - Is the the configuration tooling for the specified service - supplied by Red Hat? - - TBD: Take in a service_name and evaluate. - def is_rh_supplied(self, service_name): - ''' - return False - - def find_tooling(self, service_name): - ''' - Description: - Given a service name return the path to the configuration - tooling. - - Search for the service start executable in the user - tooling directory. - self.tool_dir + '/user/<service name>/start' - - If not found there search for the it in the documented directory - here built in tooling should be placed. - self.tool_dir + '/AUDREY_TOOLING/<service name>/start' - - If not found there search for the it in the Red Hat tooling - directory. - self.tool_dir + '/REDHAT/<service name>/start' - - If not found there raise an error. - - Returns: - return 1 - True if top level tooling found, False otherwise. - return 2 - path to tooling - ''' - - top_path = self.tool_dir + 'user/start' - if os.access(top_path, os.X_OK): - return True, top_path - - service_user_path = self.tool_dir + 'user/' + \ - service_name + '/start' - if os.access(service_user_path, os.X_OK): - return False, service_user_path - - service_redhat_path = self.tool_dir + 'AUDREY_TOOLING/' + \ - service_name + '/start' - if os.access(service_redhat_path, os.X_OK): - return False, service_redhat_path - - service_redhat_path = self.tool_dir + 'REDHAT/' + \ - service_name + '/start' - if os.access(service_redhat_path, os.X_OK): - return False, service_redhat_path - - # No tooling found. Raise an error. - _raise_ASError(('No configuration tooling found for service: %s') % \ - (service_name)) - -class CSClient(object): - ''' - Description: - Client interface to Config Server (CS) - ''' - - def __init__(self, endpoint, oauth_key, oauth_secret, **kwargs): - ''' - Description: - Set initial state so it can be tracked. Valuable for - testing and debugging. - ''' - - self.version = CS_API_VER - self.cs_endpoint = endpoint - self.cs_oauth_key = oauth_key - self.cs_oauth_secret = oauth_secret - self.ec2_user_data_url = EC2_USER_DATA_URL - self.cs_params = '' - self.cs_configs = '' - self.tmpdir = '' - self.tarball = '' - - # create an oauth client for communication with the cs - consumer = oauth.Consumer(self.cs_oauth_key, self.cs_oauth_secret) - # 2 legged auth, token unnessesary - token = None #oauth.Token('access-key-here','access-key-secret-here') - client = oauth.Client(consumer, token) - self.http = client - - def __del__(self): - ''' - Description: - Class destructor - ''' - try: - shutil.rmtree(self.tmpdir) - except OSError: - pass # ignore any errors when attempting to remove the temp dir. - - def __str__(self): - ''' - Description: - Called by the str() function and by the print statement to - produce the informal string representation of an object. - ''' - return('\n<Instance of: %s\n' \ - '\tVersion: %s\n' \ - '\tConfig Server Endpoint: %s\n' \ - '\tConfig Server oAuth Key: %s\n' \ - '\tConfig Server oAuth Secret: %s\n' \ - '\tConfig Server Params: %s\n' \ - '\tConfig Server Configs: %s\n' \ - '\tTemporary Directory: %s\n' \ - '\tTarball Name: %s\n' \ - 'eot>' % - (self.__class__.__name__, - str(self.version), - str(self.cs_endpoint), - str(self.cs_oauth_key), - str(self.cs_oauth_secret), - str(self.cs_params), - str(self.cs_configs), - str(self.tmpdir), - str(self.tarball), - )) - - def _cs_url(self, url_type): - ''' - Description: - Generate the Config Server (CS) URL. - ''' - return '%s/%s/%s/%s' % \ - (self.cs_endpoint, url_type, self.version, self.cs_oauth_key) - - def _get(self, url, headers=None): - ''' - Description: - Issue the http get to the the Config Server. - ''' - return self.http.request(url, method='GET', headers=headers) - - def _put(self, url, body=None, headers=None): - ''' - Description: - Issue the http put to the the Config Server. - ''' - return self.http.request(url, method='PUT', - body=body, headers=headers) - - def _validate_http_status(self, status): - ''' - Description: - Confirm the http status is one of: - 200 HTTP OK - Success and no more data of this type - 202 HTTP Accepted - Success and more data of this type - 404 HTTP Not Found - This may be temporary so try again - ''' - if (status != 200) and (status != 202) and (status != 404): - _raise_ASError(('Invalid HTTP status code: %s') % \ - (str(status))) - - # Public interfaces - def get_cs_configs(self): - ''' - Description: - get the required configuration from the Config Server. - ''' - LOGGER.info('Invoked CSClient.get_cs_configs()') - url = self._cs_url('configs') - headers = {'Accept': 'text/plain'} - - response, body = self._get(url, headers=headers) - self.cs_configs = body - self._validate_http_status(response.status) - - return response.status, body - - def get_cs_params(self): - ''' - Description: - get the provides parameters from the Config Server. - ''' - LOGGER.info('Invoked CSClient.get_cs_params()') - url = self._cs_url('params') - headers = {'Accept': 'text/plain'} - - response, body = self._get(url, headers=headers) - self.cs_params = body - self._validate_http_status(response.status) - - return response.status, body - - def put_cs_params_values(self, params_values): - ''' - Description: - put the provides parameters to the Config Server. - ''' - LOGGER.info('Invoked CSClient.put_cs_params_values()') - url = self._cs_url('params') - headers = {'Content-Type': 'application/x-www-form-urlencoded'} - - response, body = self._put(url, body=params_values, headers=headers) - return response.status, body - - def get_cs_tooling(self): - ''' - Description: - get any optional user supplied tooling which is - provided as a tarball - ''' - LOGGER.info('Invoked CSClient.get_cs_tooling()') - url = self._cs_url('files') - headers = {'Accept': 'content-disposition'} - - tarball = '' - response, body = self._get(url, headers=headers) - self._validate_http_status(response.status) - - # Parse the file name burried in the response header - # at: response['content-disposition'] - # as: 'attachment; tarball="tarball.tgz"' - if (response.status == 200) or (response.status == 202): - tarball = response['content-disposition']. \ - lstrip('attachment; filename=').replace('"','') - - # Create the temporary tarfile - try: - self.tmpdir = tempfile.mkdtemp() - self.tarball = self.tmpdir + '/' + tarball - f = open(self.tarball, 'w') - f.write(body) - f.close() - except IOError, (errno, strerror): - _raise_ASError(('File not found or not a tar file: %s ' + \ - 'Error: %s %s') % (self.tarball, errno, strerror)) - - return response.status, self.tarball - -def discover_config_server(cloud_info_file=CLOUD_INFO_FILE, - condor_addr_file=CONDORCLOUD_CS_ADDR, - condor_uuid_file=CONDORCLOUD_CS_UUID, - ec2_user_data=EC2_USER_DATA_URL, - http=httplib2.Http()): - ''' - Description: - Discover the Config Server access info. - If not discover it using the cloud provider specific method. - ''' - # - # What Cloud Backend? - # - # Read the file populated with Cloud back end type. - # e.g.: CLOUD_TYPE="EC2" - # - - def _parse_user_data(data, condor=None): - ''' - Take a string in form version|cs_endpoint|oauth_key|oauth_secret - and populate the respective self vars. - Conductor puts the UUID into the oauth_key field. - At minimum this function expects to find a | in the string - this is in effort not to log oauth secrets. - ''' - LOGGER.debug('Parsing User Data') - user_data = data.split('|') - if len(user_data) > 1: - if user_data[0] == '1': - if condor: - ud_version, endpoint, \ - oauth_secret = user_data - oauth_key = condor - else: - ud_version, endpoint, \ - oauth_key, oauth_secret = user_data - return {'endpoint': endpoint, - 'oauth_key': oauth_key, - 'oauth_secret': oauth_secret,} - #elif ud[0] == nextversion - # parse code for version - else: - _raise_ASError('Invalid User Data Version: %s' % user_data[0]) - else: - _raise_ASError('Could not get user data version, parse failed') - - try: - with open(cloud_info_file, 'r') as fp: - read_data = fp.read() - except IOError: - _raise_ASError(('Failed accessing file %s') % \ - (cloud_info_file)) - - # - # Discover the Config Server access info. - # - cloud_type = read_data.upper() - if 'EC2' in cloud_type: - # - # If on EC2 the user data will contain the Config Server - # access info. - # - - try: - max_attempts = 5 - headers = {'Accept': 'text/plain'} - for attempt in range(1, max_attempts): - response, body = http.request(ec2_user_data, - headers=headers) - if response.status == 200: - break - if response.status != 200: - _raise_ASError('Max attempts to get EC2 user data \ - exceeded.') - - if '|' not in body: - body = base64.b64decode(body) - return _parse_user_data(body) - - except Exception, e: - _raise_ASError('Failed accessing EC2 user data: %s' % e) - - elif 'CONDORCLOUD' in cloud_type: - # - # If on Condor Cloud, the user data will be in smbios - # Uses the dmi files to access the stored smbios information. - # - try: - return _parse_user_data(open(condor_addr_file, 'r').read().strip(), - open(condor_uuid_file, 'r').read().strip()) - except Exception, e: - _raise_ASError('Failed accessing Config Server data: %s' % e) - - elif 'RHEV' in cloud_type: - # - # If on RHEV-M the user data will be contained on the - # floppy device in file deltacloud-user-data.txt. - # To access it: - # modprobe floppy - # mount /dev/fd0 /media - # read /media/deltacloud-user-data.txt - # - # Note: - # On RHEVm the deltacloud drive had been delivering the user - # data base64 decoded at one point that changed such that the - # deltacloud drive leaves the date base64 encoded. This - # Code segment will handle both base64 encoded and decoded - # user data. - # - # Since ':' is used as a field delimiter in the user data - # and is not a valid base64 char, if ':' is found assume - # the data is already base64 decoded. - # - # modprobe floppy - cmd = ['/sbin/modprobe', 'floppy'] - ret = _run_cmd(cmd) - if ret['subproc'].returncode != 0: - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - cmd = ['/bin/mkdir', '/media'] - ret = _run_cmd(cmd) - # If /media is already there (1) or any other error (0) - if (ret['subproc'].returncode != 1) and \ - (ret['subproc'].returncode != 0): - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - cmd = ['/bin/mount', '/dev/fd0', '/media'] - ret = _run_cmd(cmd) - # If /media is already mounted (32) or any other error (0) - if (ret['subproc'].returncode != 32) and \ - (ret['subproc'].returncode != 0): - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - try: - # Condfig Server (CS) address:port. - with open('/media/deltacloud-user-data.txt', 'r') as fp: - line = fp.read().strip() - if '|' not in line: - line = base64.b64decode(line) - return _parse_user_data(line) - except: - _raise_ASError('Failed accessing RHEVm user data.') - - elif 'VSPHERE' in cloud_type: - # - # If on vSphere the user data will be contained on the - # floppy device in file deltacloud-user-data.txt. - # To access it: - # mount /dev/fd0 /media - # read /media/deltacloud-user-data.txt - # - # Note: - # On vSphere the deltacloud drive had been delivering the user - # data base64 decoded at one point that changed such that the - # deltacloud drive leaves the date base64 encoded. This - # Code segment will handle both base64 encoded and decoded - # user data. - # - # Since ':' is used as a field delimiter in the user data - # and is not a valid base64 char, if ':' is found assume - # the data is already base64 decoded. - # - cmd = ['/bin/mkdir', '/media'] - ret = _run_cmd(cmd) - # If /media is already there (1) or any other error (0) - if (ret['subproc'].returncode != 1) and \ - (ret['subproc'].returncode != 0): - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - cmd = ['/bin/mount', '/dev/cdrom', '/media'] - ret = _run_cmd(cmd) - # If /media is already mounted (32) or any other error (0) - if (ret['subproc'].returncode != 32) and \ - (ret['subproc'].returncode != 0): - _raise_ASError(('Failed command: \n%s \nError: \n%s') % \ - (' '.join(cmd), str(ret['err']))) - - try: - # Condfig Server (CS) address:port. - with open('/media/deltacloud-user-data.txt', 'r') as fp: - line = fp.read().strip() - if '|' not in line: - line = base64.b64decode(line) - return _parse_user_data(line) - except: - _raise_ASError('Failed accessing vSphere user data.') - -def setup_logging(level=logging.INFO, logfile_name=LOG): - ''' - Description: - Establish the output logging. - ''' - - global LOGGER - - # If not run as root create the log file in the current directory. - # This allows minimal functionality, e.g.: --help - if not os.geteuid() == 0: - logfile_name = './audrey.log' - - # set up logging - LOG_FORMAT = ('%(asctime)s - %(levelname)-8s: ' - '%(filename)s:%(lineno)d %(message)s') - LOG_LEVEL_INPUT = 5 - LOG_NAME_INPUT = 'INPUT' - - logging.basicConfig(filename=logfile_name, - level=level, filemode='w', format=LOG_FORMAT) - - logging.addLevelName(LOG_LEVEL_INPUT, LOG_NAME_INPUT) - - LOGGER = logging.getLogger('Audrey') - -def parse_args(): - ''' - Description: - Gather any Config Server access info optionally passed - on the command line. If being provided on the command - line all of it must be provided. - - oAuth Secret is prompted for and not allowed as an argument. - This is to avoid a ps on the system from displaying the - oAuth Secret argument. - - Return: - dict - of parser keys and values - ''' - desc_txt = 'The Aeolus Audrey Startup Agent, a script which ' + \ - 'runs on a booting cloud instance to retrieve ' + \ - 'configuration data from the Aeolus Config Server.' - - log_level_dict={'DEBUG' : logging.DEBUG, - 'INFO' : logging.INFO, - 'WARNING' : logging.WARNING, - 'ERROR' : logging.ERROR, - 'CRITICAL' : logging.CRITICAL} - - parser = argparse.ArgumentParser(description=desc_txt) - parser.add_argument('-e', '--endpoint', dest='endpoint', - required=False, help='Config Server endpoint url') - parser.add_argument('-k', '--key', dest='oauth_key', required=False, - help='oAuth Key. If specified prompt for the oAuth Secret.') - parser.add_argument('-p', '--pwd', action='store_true', default=False, - required=False, help='Log and look for configs in pwd',) - parser.add_argument('-L', '--log-level', dest='log_level', - required=False, default='INFO', help='Audrey Agent Logging Level', - choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']), - parser.add_argument('-V', '-v', '--version', dest='version', - action='store_true', default=False, required=False, - help='Displays the program's version number and exit.') - - args = parser.parse_args() - args.log_level = log_level_dict[args.log_level] - - if args.version: - print AUDREY_VER - sys.exit() - - if args.oauth_key: - # Prompt for oAuth secret so ps won't display it. - args.oauth_secret = raw_input('oAuth Secret: ') - - return args - -def audrey_script_main(client_http=None): - ''' - Description: - This script will be used on EC2 for configuring the running - instance based on Cloud Engine configuration supplied at - launch time in the user data. - - Config Server Status: - 200 HTTP OK - Success and no more data of this type - 202 HTTP Accepted - Success and more data of this type - 404 HTTP Not Found - This may be temporary so try again - ''' - # parse the args and setup logging - conf = parse_args() - if 'pwd' in conf and conf.pwd: - log_file = 'audrey.log' - tool_dir = 'tooling' - cloud_info = 'cloud_info' - else: - log_file = LOG - tool_dir = TOOLING_DIR - cloud_info = CLOUD_INFO_FILE - - setup_logging(level=conf.log_level, - logfile_name=log_file) - - if not conf.endpoint: - if client_http: - conf = discover_config_server(cloud_info_file=cloud_info, - http=client_http) - else: - # discover the cloud I'm on - conf = discover_config_server(cloud_info_file=cloud_info) - - # ensure the conf it a dictionary, not a namespace - if hasattr(conf, '__dict__'): - conf = vars(conf) - - LOGGER.info('Invoked audrey_script_main') - - # 0 means don't run again - # -1 is non zero so initial runs will happen - config_status = -1 - param_status = -1 - tooling_status = -1 - - max_retry = 5 - services = [] - - # Create the Client Object - cs_client = CSClient(**conf) - if client_http: - cs_client.http = client_http - LOGGER.info(str(cs_client)) - - LOGGER.debug('Get optional tooling from the Config Server') - # Get any optional tooling from the Config Server - tooling = ConfigTooling(tool_dir=tool_dir) - tooling_status, tarball = cs_client.get_cs_tooling() - if (tooling_status == 200) or (tooling_status == 202): - tooling.unpack_tooling(tarball) - else: - LOGGER.info('No optional config tooling provided. status: ' + \ - str(tooling_status)) - LOGGER.debug(str(tooling)) - - LOGGER.debug('Process the Requires and Provides parameters') - - # Process the Requires and Provides parameters until the HTTP status - # from the get_cs_configs and the get_cs_params both return 200 - while config_status or param_status: - - LOGGER.debug('Config Parameter status: ' + str(config_status)) - LOGGER.debug('Return Parameter status: ' + str(param_status)) - - # Get the Required Configs from the Config Server - if config_status: - config_status, configs = cs_client.get_cs_configs() - - # Configure the system with the provided Required Configs - if config_status == 200: - services = parse_require_config(configs) - tooling.invoke_tooling(services) - # don't do any more config status work - # now that the tooling has run - config_status = 0 - else: - LOGGER.info('No configuration parameters provided. status: ' + \ - str(config_status)) - - # Get the requested provides from the Config Server - if param_status: - get_status, params = cs_client.get_cs_params() - - # Gather the values from the system for the requested provides - if get_status == 200: - params_values = generate_provides(params) - else: - params_values = '||' - - # Put the requested provides with values to the Config Server - param_status, body = cs_client.put_cs_params_values(params_values) - if param_status == 200: - # don't operate on params anymore, all have been provided. - param_status = 0 - - # Retry a number of times if 404 HTTP Not Found is returned. - if config_status == 404 or param_status == 404: - LOGGER.error('Requiest to Config Server failed or more to come.') - LOGGER.error('Required Config Parameter status: ' + \ - str(config_status)) - LOGGER.info('Return Parameter status: ' + str(param_status)) - - max_retry -= 1 - if max_retry < 0: - _raise_ASError('Too many erroneous Config Server responses.') - - sleep(10) - -if __name__ == '__main__': - - audrey_script_main() diff --git a/audrey_start/test_audrey_startup.py b/audrey_start/test_audrey_startup.py deleted file mode 100644 index 4980249..0000000 --- a/audrey_start/test_audrey_startup.py +++ /dev/null @@ -1,692 +0,0 @@ -#!/usr/bin/python2.6 -''' -* -* Copyright [2011] [Red Hat, Inc.] -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -''' - -''' - test_audrey_startup.py - - Test program for audrey_startup -''' - -import base64 -import logging -import os -import os.path -import tempfile -import unittest -import sys -import tarfile - -from audrey_startup import CSClient -from audrey_startup import ConfigTooling -from audrey_startup import ASError -from audrey_startup import parse_args -from audrey_startup import parse_provides_params -from audrey_startup import parse_require_config -from audrey_startup import audrey_script_main -from audrey_startup import gen_env -from audrey_startup import _run_cmd, _run_pipe_cmd -from audrey_startup import generate_provides -from audrey_startup import setup_logging -from audrey_startup import discover_config_server - -# Helpers and utils -DUMMY_USER_DATA = '1|http://example.com/%7CoauthConsumer%7CoauthSecret' -DUMMY_CS_CONFIG = {'endpoint': 'http://example.com/', - 'oauth_key': 'oauthConsumer', - 'oauth_secret': 'oauthSecret',} - -try: - from cStringIO import StringIO as BIO -except ImportError: # python 3 - from io import BytesIO as BIO - -class HttpUnitTest(object): - ''' - Description: - When testing the http object does not exists. This class provides - test methods that could be preformed when doing UNITTESTing. - ''' - class HttpUnitTestResponse(object): - ''' - Description: - When testing the http object does not exists. This class - provides the test method response that could be preformed - when doing UNITTESTing. - ''' - def __init__(self, status): - self.status = status - - def add_content_disposition(self): - self.__dict__['content-disposition'] = \ - 'attachment; filename=test.tar.gz' - - def __getitem__(self, key): - return self.__dict__[key] - - - # simple HTTP Response with 200 status code - ok_response = HttpUnitTestResponse(200) - not_found_response = HttpUnitTestResponse(404) - - def request(self, url, method='GET', body=None, headers=None): - ''' - Handle request when not running live but in test environment. - ''' - body = '' - response = HttpUnitTest.ok_response - if method == 'GET': - if url.find('/configs/') > -1: - body = '|service|s1|parameters|param1&%s|param2&%s|' % \ - (base64.b64encode('value1'), base64.b64encode('value2')) - elif url.find('/params/') > -1: - body = '|param1¶m2|' - elif url.find('/files/') > -1: - file_out = BIO() - tar = tarfile.open(mode = "w:gz", fileobj = file_out) - tar.add('/etc/passwd') - tar.close() - body = file_out.getvalue() - response.add_content_disposition() - elif url.endswith('/user-data'): - body = base64.b64encode(DUMMY_USER_DATA) - elif url.endswith('/no-version-user-data'): - body = base64.b64encode('0|endpoint') - elif url.endswith('/empty-user-data'): - body = base64.b64encode('') - elif url.endswith('/gimmie-404'): - body = base64.b64encode(DUMMY_USER_DATA) - response = HttpUnitTest.not_found_response - else: - print url - response = HttpUnitTest.not_found_response - #elif method == 'POST' and url.find('/params/') > -1: - # body = '' - return response, body - -def _write_info_file(filepath, cloud): - f = open(filepath, 'w') - f.write(cloud) - f.close() - -# The actual tests - -class TestAudreyStarupRunCmds(unittest.TestCase): - ''' - Test the _run*cmd functions - ''' - def test_success_run_pipe_cmd(self): - self.assertEqual("'test'\n", - _run_pipe_cmd(["echo", "'test'"], ["grep", "test"])['out']) - - def test_cmd2_fail_run_pipe_cmd(self): - self.assertEqual("[Errno 2] No such file or directory", - _run_pipe_cmd(["echo", "'test'"], ["notreal"])['err']) - - def test_cmd1_fail_run_pipe_cmd(self): - self.assertEqual("[Errno 2] No such file or directory", - _run_pipe_cmd(["notreal"], ["echo", "'test'"])['err']) - -class TestAudreyStartupConfigTooling(unittest.TestCase): - ''' - Make sure all the Config tooling is tested - ''' - def test_is_user_supplied(self): - ConfigTooling('test_tooling').is_user_supplied() - - def test_is_rh_supplied(self): - ConfigTooling('test_tooling').is_rh_supplied() - - def test_empty_find_tooling(self): - self.assertRaises(ASError, ConfigTooling('test_tooling').find_tooling, '') - - def test_fail_to_create_tooling_dir(self): - self.assertRaises(ASError, ConfigTooling, tool_dir='/not/real/dir') - -class TestAudreyStartupRequiredConfig(unittest.TestCase): - ''' - Class for exercising the parsing of the Required Configs from the CS. - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, './test_audrey_startup.log') - - def test_success_service_n_params(self): - ''' - Success case: - - Exercise parse_require_config() with valid input - ''' - # Establish valid test data: - - src = '|service|jon1' + \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon1_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon1_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon1_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_success_service_n_params()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - - # Validate results - self.assertEqual(services[0].name, 'jon1') - self.assertEqual(services[1].name, 'jon2') - - for service in services: - for param in service.params: - name_val = param.split('&') - env_var = 'AUDREY_VAR_' + service.name + '_' + name_val[0] - print 'name_val[0]: ' + str(name_val[0]) - print 'param: ' + str(param) - print 'services.name: ' + str(service.name) - - cmd = ['/usr/bin/printenv', env_var] - ret = _run_cmd(cmd) - self.assertEqual(ret['out'][:-1], \ - validation_dict[env_var]) - - def test_success_empty_source(self): - ''' - Success case: - - Exercise parse_require_config() with valid empty input - ''' - - # Establish valid test data: - src = '||' - print '\nTest Name: test_success_empty_source()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - print 'services: ' + str(services) - - # Validate results - self.assertEqual(services, []) - - def test_success_empty_service(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - ''' - - # Establish valid test data: - src = '|service|' + \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_success_empty_service()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - - # Validate results - self.assertEqual(services[0].name, '') - self.assertEqual(services[1].name, 'jon2') - - for service in services: - for param in service.params: - name_val = param.split('&') - env_var = 'AUDREY_VAR_' + name_val[0] - - print 'name_val[0]: ' + str(name_val[0]) - print 'param: ' + str(param) - print 'services.name: ' + str(service.name) - - cmd = ['/usr/bin/printenv', env_var] - ret = _run_cmd(cmd) - self.assertEqual(ret['out'][:-1], \ - validation_dict[env_var]) - - def test_failure_no_services_name(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - - The slight difference between this test and test_success_empty_services - is the success case has an empty service name indicated by "||": - |service||paramseters - - and the failure case has no service name: - |service|paramseters - - ''' - - # Establish valid test data: - src = '|service' \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_failure_no_service_names()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - with self.assertRaises(ASError): - print 'parse_require_config returned: ' + \ - str(parse_require_config(src)) - - def test_failure_bad_service_name(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - ''' - - # Establish valid test data: - src = '|service|parameters|' - print '\nTest Name: test_failure_bad_service_name()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() ASError' - - # Exersise code segment - with self.assertRaises(ASError): - print 'parse_require_config returned: ' + \ - str(parse_require_config(src)) - -class TestAudreyStartupDiscovery(unittest.TestCase): - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, 'test_audrey_startup.log') - self.cloud_info_file = 'cloud_info' - self.condor_addr_file = 'condor_addr' - self.condor_uuid_file = 'condor_uuid' - - def tearDown(self): - os.remove(self.cloud_info_file) - if os.path.exists(self.condor_addr_file): - os.remove(self.condor_addr_file) - if os.path.exists(self.condor_uuid_file): - os.remove(self.condor_uuid_file) - - def test_ec2(self): - _write_info_file(self.cloud_info_file, 'EC2') - discover_config_server(self.cloud_info_file, http=HttpUnitTest()) - - def test_ec2_404(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/gimmie-404') - - def test_condorcloud(self): - _write_info_file(self.condor_addr_file, '1|endpoint|secret') - _write_info_file(self.condor_uuid_file, 'key') - _write_info_file(self.cloud_info_file, 'CONDORCLOUD') - discover_config_server(self.cloud_info_file, - condor_addr_file=self.condor_addr_file, - condor_uuid_file=self.condor_uuid_file) - - def test_rhev(self): - _write_info_file(self.cloud_info_file, 'RHEV') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file) - - def test_vsphere(self): - _write_info_file(self.cloud_info_file, 'VSPHERE') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file) - - def test_invalid_user_data_version(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/no-version-user-data') - - def test_invalid_user_data_no_delim(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/empty-user-data') - - -class TestAudreyStartupProvidesParameters(unittest.TestCase): - ''' - Class for exercising the parsing of the Provides ParametersConfigs - from the CS. - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, './test_audrey_startup.log') - - def test_success_parameters(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - ''' - - # Establish valid test data: - src = '|operatingsystem&is_virtual|' - - print '\nTest Name: test_success_parameters()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['operatingsystem', 'is_virtual'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - print 'len(provides): ' + str(len(provides)) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - self.assertTrue('audrey_data=%7Coperatingsystem' in provides) - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - def test_success_no_params(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - Containging an unavailable parameter - ''' - - # Establish valid test data: - src = '|uptime_days&unavailable_dogs&ipaddress|' - - print '\nTest Name: test_success_no_params()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['uptime_days', 'unavailable_dogs', 'ipaddress'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - # Confirm unavailable parameters return an empty string. - self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) - - def test_success_one_parameters(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - with only one parameter - ''' - - # Establish valid test data: - src = '|uptime_days|' - - print '\nTest Name: test_success_parameters()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['uptime_days'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - def test_success_one_parameter(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - With only one parameter which is unavailable - ''' - - # Establish valid test data: - src = '|unavailable_dogs|' - - print '\nTest Name: test_success_one_parameter()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['unavailable_dogs'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - # Confirm unavailable parameters return an empty string. - self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) - - def test_failure_missing_delimiter(self): - ''' - Failure case: - - Exercise parse_provides_params() and generate_provides() - with invalid input - - missing leading delimiter - ''' - - # Establish valid test data: - src = 'unavailable_dogs|' - - print '\nTest Name: test_failure_missing_delimiter()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() ASError' - - expected_params_list = ['unavailable_dogs'] - - # Exersise code segment and validate results - with self.assertRaises(ASError): - params_list = parse_provides_params(src) - - with self.assertRaises(ASError): - provides = generate_provides(src) - -class TestConfigServerClient(unittest.TestCase): - ''' - Class for exercising the gets and put to and from the CS - ''' - - def setUp(self): - ''' - If the cloud info file is not present assume running in a - UNITTEST environment. This will allow for exercising some - of the code without having to be running in a cloud VM. - - Set up logging. - ''' - - setup_logging(logging.DEBUG, './test_audrey_startup.log') - - # Create the client Object - self.cs_client = CSClient(**DUMMY_CS_CONFIG) - self.cs_client.http = HttpUnitTest() - - def tearDown(self): - pass - - def test_success_get_cs_configs(self): - ''' - Success case: - - Exercise get_cs_configs() - ''' - print '\n\n--- Test Name: test_success_get_cs_configs ---' - - self.cs_client.get_cs_configs() - - # Add asserts - print 'test_success_get_cs_configs() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - - def test_success_get_cs_tooling(self): - ''' - Success case: - - Exercise get_cs_tooling() - ''' - self.cs_client.get_cs_tooling() - - def test_success_get_cs_params(self): - ''' - Success case: - - Exercise get_cs_params() - ''' - print '\n\n--- Test Name: test_success_get_cs_params ---' - - self.cs_client.get_cs_params() - - # Add asserts - print 'test_success_get_cs_params() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - - def test_success_get_cs_confs_n_params(self): - ''' - Success case: - - Exercise get_cs_configs() and get_cs_params() - ''' - print '\n\n--- Test Name: test_success_get_cs_confs_and_params ---' - - self.cs_client.get_cs_configs() - self.cs_client.get_cs_params() - - # Add asserts - print 'test_success_get_cs_confs_n_params() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - print 'test_success_get_cs_confs_n_params() Add asserts' - - def test_success_put_cs_params_values(self): - ''' - Success case: - - Exercise put_cs_params_values() - ''' - self.cs_client.put_cs_params_values('') - - def test_error_http_status(self): - ''' - Success case: - - Exercise put_cs_params_values() - ''' - self.assertRaises(ASError, self.cs_client._validate_http_status, 401) - -class TestAudreyScript(unittest.TestCase): - ''' - Class for exercising the full audrey script functionality - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - - This test currently require to be run in a cloud VM - with a live Config Server. - ''' - setup_logging(logging.DEBUG, './test_audrey_startup.log') - # make a copy of argv - self.argv = list(sys.argv) - - def tearDown(self): - # replace argv - sys.argv = list(self.argv) - - def test_audrey_script_main(self): - ''' - Perform what the audrey script will do. - ''' - cloud_info_file = 'cloud_info' - sys.argv.extend(['-p']) - _write_info_file(cloud_info_file, 'EC2') - audrey_script_main(HttpUnitTest()) - os.remove(cloud_info_file) - - def test_fail_audrey_script_main(self): - ''' - Perform what the audrey script will do. - ''' - self.assertRaises(ASError, audrey_script_main) - - def test_empty_gen_env(self): - self.assertRaises(ASError, gen_env, '', '') - - # doesn't actually test what I wanted it to. - #def test_parse_require_config(self): - # self.assertRaises(ASError, parse_require_config, '') - -if __name__ == '__main__': - - setup_logging(logging.DEBUG, logfile_name='./test_audrey_startup.log') - unittest.main()
--- agent/test_audrey_agent.py | 692 ++++++++++++++++++++++++++++++++++++++++++ agent/test_audrey_startup.py | 692 ------------------------------------------ 2 files changed, 692 insertions(+), 692 deletions(-) create mode 100644 agent/test_audrey_agent.py delete mode 100644 agent/test_audrey_startup.py
diff --git a/agent/test_audrey_agent.py b/agent/test_audrey_agent.py new file mode 100644 index 0000000..d87c05f --- /dev/null +++ b/agent/test_audrey_agent.py @@ -0,0 +1,692 @@ +#!/usr/bin/python2.6 +''' +* +* Copyright [2011] [Red Hat, Inc.] +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +* +''' + +''' + test_audrey_agent.py + + Test program for audrey_agent +''' + +import base64 +import logging +import os +import os.path +import tempfile +import unittest +import sys +import tarfile + +from audrey_agent import CSClient +from audrey_agent import ConfigTooling +from audrey_agent import ASError +from audrey_agent import parse_args +from audrey_agent import parse_provides_params +from audrey_agent import parse_require_config +from audrey_agent import audrey_script_main +from audrey_agent import gen_env +from audrey_agent import _run_cmd, _run_pipe_cmd +from audrey_agent import generate_provides +from audrey_agent import setup_logging +from audrey_agent import discover_config_server + +# Helpers and utils +DUMMY_USER_DATA = '1|http://example.com/%7CoauthConsumer%7CoauthSecret' +DUMMY_CS_CONFIG = {'endpoint': 'http://example.com/', + 'oauth_key': 'oauthConsumer', + 'oauth_secret': 'oauthSecret',} + +try: + from cStringIO import StringIO as BIO +except ImportError: # python 3 + from io import BytesIO as BIO + +class HttpUnitTest(object): + ''' + Description: + When testing the http object does not exists. This class provides + test methods that could be preformed when doing UNITTESTing. + ''' + class HttpUnitTestResponse(object): + ''' + Description: + When testing the http object does not exists. This class + provides the test method response that could be preformed + when doing UNITTESTing. + ''' + def __init__(self, status): + self.status = status + + def add_content_disposition(self): + self.__dict__['content-disposition'] = \ + 'attachment; filename=test.tar.gz' + + def __getitem__(self, key): + return self.__dict__[key] + + + # simple HTTP Response with 200 status code + ok_response = HttpUnitTestResponse(200) + not_found_response = HttpUnitTestResponse(404) + + def request(self, url, method='GET', body=None, headers=None): + ''' + Handle request when not running live but in test environment. + ''' + body = '' + response = HttpUnitTest.ok_response + if method == 'GET': + if url.find('/configs/') > -1: + body = '|service|s1|parameters|param1&%s|param2&%s|' % \ + (base64.b64encode('value1'), base64.b64encode('value2')) + elif url.find('/params/') > -1: + body = '|param1¶m2|' + elif url.find('/files/') > -1: + file_out = BIO() + tar = tarfile.open(mode = "w:gz", fileobj = file_out) + tar.add('/etc/passwd') + tar.close() + body = file_out.getvalue() + response.add_content_disposition() + elif url.endswith('/user-data'): + body = base64.b64encode(DUMMY_USER_DATA) + elif url.endswith('/no-version-user-data'): + body = base64.b64encode('0|endpoint') + elif url.endswith('/empty-user-data'): + body = base64.b64encode('') + elif url.endswith('/gimmie-404'): + body = base64.b64encode(DUMMY_USER_DATA) + response = HttpUnitTest.not_found_response + else: + print url + response = HttpUnitTest.not_found_response + #elif method == 'POST' and url.find('/params/') > -1: + # body = '' + return response, body + +def _write_info_file(filepath, cloud): + f = open(filepath, 'w') + f.write(cloud) + f.close() + +# The actual tests + +class TestAudreyStarupRunCmds(unittest.TestCase): + ''' + Test the _run*cmd functions + ''' + def test_success_run_pipe_cmd(self): + self.assertEqual("'test'\n", + _run_pipe_cmd(["echo", "'test'"], ["grep", "test"])['out']) + + def test_cmd2_fail_run_pipe_cmd(self): + self.assertEqual("[Errno 2] No such file or directory", + _run_pipe_cmd(["echo", "'test'"], ["notreal"])['err']) + + def test_cmd1_fail_run_pipe_cmd(self): + self.assertEqual("[Errno 2] No such file or directory", + _run_pipe_cmd(["notreal"], ["echo", "'test'"])['err']) + +class TestAudreyStartupConfigTooling(unittest.TestCase): + ''' + Make sure all the Config tooling is tested + ''' + def test_is_user_supplied(self): + ConfigTooling('test_tooling').is_user_supplied() + + def test_is_rh_supplied(self): + ConfigTooling('test_tooling').is_rh_supplied() + + def test_empty_find_tooling(self): + self.assertRaises(ASError, ConfigTooling('test_tooling').find_tooling, '') + + def test_fail_to_create_tooling_dir(self): + self.assertRaises(ASError, ConfigTooling, tool_dir='/not/real/dir') + +class TestAudreyStartupRequiredConfig(unittest.TestCase): + ''' + Class for exercising the parsing of the Required Configs from the CS. + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + def test_success_service_n_params(self): + ''' + Success case: + - Exercise parse_require_config() with valid input + ''' + # Establish valid test data: + + src = '|service|jon1' + \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon1_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon1_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon1_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_success_service_n_params()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + + # Validate results + self.assertEqual(services[0].name, 'jon1') + self.assertEqual(services[1].name, 'jon2') + + for service in services: + for param in service.params: + name_val = param.split('&') + env_var = 'AUDREY_VAR_' + service.name + '_' + name_val[0] + print 'name_val[0]: ' + str(name_val[0]) + print 'param: ' + str(param) + print 'services.name: ' + str(service.name) + + cmd = ['/usr/bin/printenv', env_var] + ret = _run_cmd(cmd) + self.assertEqual(ret['out'][:-1], \ + validation_dict[env_var]) + + def test_success_empty_source(self): + ''' + Success case: + - Exercise parse_require_config() with valid empty input + ''' + + # Establish valid test data: + src = '||' + print '\nTest Name: test_success_empty_source()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + print 'services: ' + str(services) + + # Validate results + self.assertEqual(services, []) + + def test_success_empty_service(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + ''' + + # Establish valid test data: + src = '|service|' + \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_success_empty_service()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + services = parse_require_config(src) + + # Validate results + self.assertEqual(services[0].name, '') + self.assertEqual(services[1].name, 'jon2') + + for service in services: + for param in service.params: + name_val = param.split('&') + env_var = 'AUDREY_VAR_' + name_val[0] + + print 'name_val[0]: ' + str(name_val[0]) + print 'param: ' + str(param) + print 'services.name: ' + str(service.name) + + cmd = ['/usr/bin/printenv', env_var] + ret = _run_cmd(cmd) + self.assertEqual(ret['out'][:-1], \ + validation_dict[env_var]) + + def test_failure_no_services_name(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + + The slight difference between this test and test_success_empty_services + is the success case has an empty service name indicated by "||": + |service||paramseters + + and the failure case has no service name: + |service|paramseters + + ''' + + # Establish valid test data: + src = '|service' \ + '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ + '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ + '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ + '|service|jon2|' + + validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', + 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', + 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } + + print '\nTest Name: test_failure_no_service_names()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() success' + + # Exersise code segment + with self.assertRaises(ASError): + print 'parse_require_config returned: ' + \ + str(parse_require_config(src)) + + def test_failure_bad_service_name(self): + ''' + Failure case: + - Exercise parse_require_config() with valid input + ''' + + # Establish valid test data: + src = '|service|parameters|' + print '\nTest Name: test_failure_bad_service_name()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() ASError' + + # Exersise code segment + with self.assertRaises(ASError): + print 'parse_require_config returned: ' + \ + str(parse_require_config(src)) + +class TestAudreyStartupDiscovery(unittest.TestCase): + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, 'test_audrey_agent.log') + self.cloud_info_file = 'cloud_info' + self.condor_addr_file = 'condor_addr' + self.condor_uuid_file = 'condor_uuid' + + def tearDown(self): + os.remove(self.cloud_info_file) + if os.path.exists(self.condor_addr_file): + os.remove(self.condor_addr_file) + if os.path.exists(self.condor_uuid_file): + os.remove(self.condor_uuid_file) + + def test_ec2(self): + _write_info_file(self.cloud_info_file, 'EC2') + discover_config_server(self.cloud_info_file, http=HttpUnitTest()) + + def test_ec2_404(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/gimmie-404') + + def test_condorcloud(self): + _write_info_file(self.condor_addr_file, '1|endpoint|secret') + _write_info_file(self.condor_uuid_file, 'key') + _write_info_file(self.cloud_info_file, 'CONDORCLOUD') + discover_config_server(self.cloud_info_file, + condor_addr_file=self.condor_addr_file, + condor_uuid_file=self.condor_uuid_file) + + def test_rhev(self): + _write_info_file(self.cloud_info_file, 'RHEV') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file) + + def test_vsphere(self): + _write_info_file(self.cloud_info_file, 'VSPHERE') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file) + + def test_invalid_user_data_version(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/no-version-user-data') + + def test_invalid_user_data_no_delim(self): + _write_info_file(self.cloud_info_file, 'EC2') + self.assertRaises(ASError, + discover_config_server, self.cloud_info_file, http=HttpUnitTest(), + ec2_user_data='http://169.254.169.254/empty-user-data') + + +class TestAudreyStartupProvidesParameters(unittest.TestCase): + ''' + Class for exercising the parsing of the Provides ParametersConfigs + from the CS. + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + def test_success_parameters(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + ''' + + # Establish valid test data: + src = '|operatingsystem&is_virtual|' + + print '\nTest Name: test_success_parameters()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['operatingsystem', 'is_virtual'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + print 'len(provides): ' + str(len(provides)) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + self.assertTrue('audrey_data=%7Coperatingsystem' in provides) + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + def test_success_no_params(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - Containging an unavailable parameter + ''' + + # Establish valid test data: + src = '|uptime_days&unavailable_dogs&ipaddress|' + + print '\nTest Name: test_success_no_params()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['uptime_days', 'unavailable_dogs', 'ipaddress'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + # Confirm unavailable parameters return an empty string. + self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) + + def test_success_one_parameters(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - with only one parameter + ''' + + # Establish valid test data: + src = '|uptime_days|' + + print '\nTest Name: test_success_parameters()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['uptime_days'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + def test_success_one_parameter(self): + ''' + Success case: + - Exercise parse_provides_params() and generate_provides() + with valid input + - With only one parameter which is unavailable + ''' + + # Establish valid test data: + src = '|unavailable_dogs|' + + print '\nTest Name: test_success_one_parameter()' + print 'Test input:\n' + src + print 'Expect: parse_provides_params() success' + + expected_params_list = ['unavailable_dogs'] + + # Exersise code segment + params_list = parse_provides_params(src) + provides = generate_provides(src) + print 'src: ' + str(src) + print 'params_list: ' + str(params_list) + print 'provides: ' + str(provides) + + # Validate results + self.assertEqual(params_list, expected_params_list) + + # The values are not validatable because they are unpredictable + # but all the expected parameters should be returned. + # Note: %7C is the encoded |, %26 is the encoded & + for param in expected_params_list: + self.assertTrue('%7C' + str(param) in provides) + + # Confirm unavailable parameters return an empty string. + self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) + + def test_failure_missing_delimiter(self): + ''' + Failure case: + - Exercise parse_provides_params() and generate_provides() + with invalid input + - missing leading delimiter + ''' + + # Establish valid test data: + src = 'unavailable_dogs|' + + print '\nTest Name: test_failure_missing_delimiter()' + print 'Test input:\n' + src + print 'Expect: parse_require_config() ASError' + + expected_params_list = ['unavailable_dogs'] + + # Exersise code segment and validate results + with self.assertRaises(ASError): + params_list = parse_provides_params(src) + + with self.assertRaises(ASError): + provides = generate_provides(src) + +class TestConfigServerClient(unittest.TestCase): + ''' + Class for exercising the gets and put to and from the CS + ''' + + def setUp(self): + ''' + If the cloud info file is not present assume running in a + UNITTEST environment. This will allow for exercising some + of the code without having to be running in a cloud VM. + + Set up logging. + ''' + + setup_logging(logging.DEBUG, './test_audrey_agent.log') + + # Create the client Object + self.cs_client = CSClient(**DUMMY_CS_CONFIG) + self.cs_client.http = HttpUnitTest() + + def tearDown(self): + pass + + def test_success_get_cs_configs(self): + ''' + Success case: + - Exercise get_cs_configs() + ''' + print '\n\n--- Test Name: test_success_get_cs_configs ---' + + self.cs_client.get_cs_configs() + + # Add asserts + print 'test_success_get_cs_configs() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + + def test_success_get_cs_tooling(self): + ''' + Success case: + - Exercise get_cs_tooling() + ''' + self.cs_client.get_cs_tooling() + + def test_success_get_cs_params(self): + ''' + Success case: + - Exercise get_cs_params() + ''' + print '\n\n--- Test Name: test_success_get_cs_params ---' + + self.cs_client.get_cs_params() + + # Add asserts + print 'test_success_get_cs_params() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + + def test_success_get_cs_confs_n_params(self): + ''' + Success case: + - Exercise get_cs_configs() and get_cs_params() + ''' + print '\n\n--- Test Name: test_success_get_cs_confs_and_params ---' + + self.cs_client.get_cs_configs() + self.cs_client.get_cs_params() + + # Add asserts + print 'test_success_get_cs_confs_n_params() Add asserts' + print 'self.cs_client : START \n' + str(self.cs_client) + \ + '\nself.cs_client : END' + print 'test_success_get_cs_confs_n_params() Add asserts' + + def test_success_put_cs_params_values(self): + ''' + Success case: + - Exercise put_cs_params_values() + ''' + self.cs_client.put_cs_params_values('') + + def test_error_http_status(self): + ''' + Success case: + - Exercise put_cs_params_values() + ''' + self.assertRaises(ASError, self.cs_client._validate_http_status, 401) + +class TestAudreyScript(unittest.TestCase): + ''' + Class for exercising the full audrey script functionality + ''' + + def setUp(self): + ''' + Perform required setup including setting up logging. + + This test currently require to be run in a cloud VM + with a live Config Server. + ''' + setup_logging(logging.DEBUG, './test_audrey_agent.log') + # make a copy of argv + self.argv = list(sys.argv) + + def tearDown(self): + # replace argv + sys.argv = list(self.argv) + + def test_audrey_script_main(self): + ''' + Perform what the audrey script will do. + ''' + cloud_info_file = 'cloud_info' + sys.argv.extend(['-p']) + _write_info_file(cloud_info_file, 'EC2') + audrey_script_main(HttpUnitTest()) + os.remove(cloud_info_file) + + def test_fail_audrey_script_main(self): + ''' + Perform what the audrey script will do. + ''' + self.assertRaises(ASError, audrey_script_main) + + def test_empty_gen_env(self): + self.assertRaises(ASError, gen_env, '', '') + + # doesn't actually test what I wanted it to. + #def test_parse_require_config(self): + # self.assertRaises(ASError, parse_require_config, '') + +if __name__ == '__main__': + + setup_logging(logging.DEBUG, logfile_name='./test_audrey_agent.log') + unittest.main() diff --git a/agent/test_audrey_startup.py b/agent/test_audrey_startup.py deleted file mode 100644 index d87c05f..0000000 --- a/agent/test_audrey_startup.py +++ /dev/null @@ -1,692 +0,0 @@ -#!/usr/bin/python2.6 -''' -* -* Copyright [2011] [Red Hat, Inc.] -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -* -''' - -''' - test_audrey_agent.py - - Test program for audrey_agent -''' - -import base64 -import logging -import os -import os.path -import tempfile -import unittest -import sys -import tarfile - -from audrey_agent import CSClient -from audrey_agent import ConfigTooling -from audrey_agent import ASError -from audrey_agent import parse_args -from audrey_agent import parse_provides_params -from audrey_agent import parse_require_config -from audrey_agent import audrey_script_main -from audrey_agent import gen_env -from audrey_agent import _run_cmd, _run_pipe_cmd -from audrey_agent import generate_provides -from audrey_agent import setup_logging -from audrey_agent import discover_config_server - -# Helpers and utils -DUMMY_USER_DATA = '1|http://example.com/%7CoauthConsumer%7CoauthSecret' -DUMMY_CS_CONFIG = {'endpoint': 'http://example.com/', - 'oauth_key': 'oauthConsumer', - 'oauth_secret': 'oauthSecret',} - -try: - from cStringIO import StringIO as BIO -except ImportError: # python 3 - from io import BytesIO as BIO - -class HttpUnitTest(object): - ''' - Description: - When testing the http object does not exists. This class provides - test methods that could be preformed when doing UNITTESTing. - ''' - class HttpUnitTestResponse(object): - ''' - Description: - When testing the http object does not exists. This class - provides the test method response that could be preformed - when doing UNITTESTing. - ''' - def __init__(self, status): - self.status = status - - def add_content_disposition(self): - self.__dict__['content-disposition'] = \ - 'attachment; filename=test.tar.gz' - - def __getitem__(self, key): - return self.__dict__[key] - - - # simple HTTP Response with 200 status code - ok_response = HttpUnitTestResponse(200) - not_found_response = HttpUnitTestResponse(404) - - def request(self, url, method='GET', body=None, headers=None): - ''' - Handle request when not running live but in test environment. - ''' - body = '' - response = HttpUnitTest.ok_response - if method == 'GET': - if url.find('/configs/') > -1: - body = '|service|s1|parameters|param1&%s|param2&%s|' % \ - (base64.b64encode('value1'), base64.b64encode('value2')) - elif url.find('/params/') > -1: - body = '|param1¶m2|' - elif url.find('/files/') > -1: - file_out = BIO() - tar = tarfile.open(mode = "w:gz", fileobj = file_out) - tar.add('/etc/passwd') - tar.close() - body = file_out.getvalue() - response.add_content_disposition() - elif url.endswith('/user-data'): - body = base64.b64encode(DUMMY_USER_DATA) - elif url.endswith('/no-version-user-data'): - body = base64.b64encode('0|endpoint') - elif url.endswith('/empty-user-data'): - body = base64.b64encode('') - elif url.endswith('/gimmie-404'): - body = base64.b64encode(DUMMY_USER_DATA) - response = HttpUnitTest.not_found_response - else: - print url - response = HttpUnitTest.not_found_response - #elif method == 'POST' and url.find('/params/') > -1: - # body = '' - return response, body - -def _write_info_file(filepath, cloud): - f = open(filepath, 'w') - f.write(cloud) - f.close() - -# The actual tests - -class TestAudreyStarupRunCmds(unittest.TestCase): - ''' - Test the _run*cmd functions - ''' - def test_success_run_pipe_cmd(self): - self.assertEqual("'test'\n", - _run_pipe_cmd(["echo", "'test'"], ["grep", "test"])['out']) - - def test_cmd2_fail_run_pipe_cmd(self): - self.assertEqual("[Errno 2] No such file or directory", - _run_pipe_cmd(["echo", "'test'"], ["notreal"])['err']) - - def test_cmd1_fail_run_pipe_cmd(self): - self.assertEqual("[Errno 2] No such file or directory", - _run_pipe_cmd(["notreal"], ["echo", "'test'"])['err']) - -class TestAudreyStartupConfigTooling(unittest.TestCase): - ''' - Make sure all the Config tooling is tested - ''' - def test_is_user_supplied(self): - ConfigTooling('test_tooling').is_user_supplied() - - def test_is_rh_supplied(self): - ConfigTooling('test_tooling').is_rh_supplied() - - def test_empty_find_tooling(self): - self.assertRaises(ASError, ConfigTooling('test_tooling').find_tooling, '') - - def test_fail_to_create_tooling_dir(self): - self.assertRaises(ASError, ConfigTooling, tool_dir='/not/real/dir') - -class TestAudreyStartupRequiredConfig(unittest.TestCase): - ''' - Class for exercising the parsing of the Required Configs from the CS. - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, './test_audrey_agent.log') - - def test_success_service_n_params(self): - ''' - Success case: - - Exercise parse_require_config() with valid input - ''' - # Establish valid test data: - - src = '|service|jon1' + \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon1_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon1_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon1_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_success_service_n_params()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - - # Validate results - self.assertEqual(services[0].name, 'jon1') - self.assertEqual(services[1].name, 'jon2') - - for service in services: - for param in service.params: - name_val = param.split('&') - env_var = 'AUDREY_VAR_' + service.name + '_' + name_val[0] - print 'name_val[0]: ' + str(name_val[0]) - print 'param: ' + str(param) - print 'services.name: ' + str(service.name) - - cmd = ['/usr/bin/printenv', env_var] - ret = _run_cmd(cmd) - self.assertEqual(ret['out'][:-1], \ - validation_dict[env_var]) - - def test_success_empty_source(self): - ''' - Success case: - - Exercise parse_require_config() with valid empty input - ''' - - # Establish valid test data: - src = '||' - print '\nTest Name: test_success_empty_source()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - print 'services: ' + str(services) - - # Validate results - self.assertEqual(services, []) - - def test_success_empty_service(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - ''' - - # Establish valid test data: - src = '|service|' + \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_success_empty_service()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - services = parse_require_config(src) - - # Validate results - self.assertEqual(services[0].name, '') - self.assertEqual(services[1].name, 'jon2') - - for service in services: - for param in service.params: - name_val = param.split('&') - env_var = 'AUDREY_VAR_' + name_val[0] - - print 'name_val[0]: ' + str(name_val[0]) - print 'param: ' + str(param) - print 'services.name: ' + str(service.name) - - cmd = ['/usr/bin/printenv', env_var] - ret = _run_cmd(cmd) - self.assertEqual(ret['out'][:-1], \ - validation_dict[env_var]) - - def test_failure_no_services_name(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - - The slight difference between this test and test_success_empty_services - is the success case has an empty service name indicated by "||": - |service||paramseters - - and the failure case has no service name: - |service|paramseters - - ''' - - # Establish valid test data: - src = '|service' \ - '|parameters|jon_server_ip&' + base64.b64encode('192.168.1.1') + \ - '|jon_server_ip_2&' + base64.b64encode('192.168.1.2') + \ - '|jon_server_ip_3&' + base64.b64encode('192.168.1.3') + \ - '|service|jon2|' - - validation_dict = {'AUDREY_VAR_jon_server_ip' : '192.168.1.1', - 'AUDREY_VAR_jon_server_ip_2' : '192.168.1.2', - 'AUDREY_VAR_jon_server_ip_3' : '192.168.1.3' } - - print '\nTest Name: test_failure_no_service_names()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() success' - - # Exersise code segment - with self.assertRaises(ASError): - print 'parse_require_config returned: ' + \ - str(parse_require_config(src)) - - def test_failure_bad_service_name(self): - ''' - Failure case: - - Exercise parse_require_config() with valid input - ''' - - # Establish valid test data: - src = '|service|parameters|' - print '\nTest Name: test_failure_bad_service_name()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() ASError' - - # Exersise code segment - with self.assertRaises(ASError): - print 'parse_require_config returned: ' + \ - str(parse_require_config(src)) - -class TestAudreyStartupDiscovery(unittest.TestCase): - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, 'test_audrey_agent.log') - self.cloud_info_file = 'cloud_info' - self.condor_addr_file = 'condor_addr' - self.condor_uuid_file = 'condor_uuid' - - def tearDown(self): - os.remove(self.cloud_info_file) - if os.path.exists(self.condor_addr_file): - os.remove(self.condor_addr_file) - if os.path.exists(self.condor_uuid_file): - os.remove(self.condor_uuid_file) - - def test_ec2(self): - _write_info_file(self.cloud_info_file, 'EC2') - discover_config_server(self.cloud_info_file, http=HttpUnitTest()) - - def test_ec2_404(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/gimmie-404') - - def test_condorcloud(self): - _write_info_file(self.condor_addr_file, '1|endpoint|secret') - _write_info_file(self.condor_uuid_file, 'key') - _write_info_file(self.cloud_info_file, 'CONDORCLOUD') - discover_config_server(self.cloud_info_file, - condor_addr_file=self.condor_addr_file, - condor_uuid_file=self.condor_uuid_file) - - def test_rhev(self): - _write_info_file(self.cloud_info_file, 'RHEV') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file) - - def test_vsphere(self): - _write_info_file(self.cloud_info_file, 'VSPHERE') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file) - - def test_invalid_user_data_version(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/no-version-user-data') - - def test_invalid_user_data_no_delim(self): - _write_info_file(self.cloud_info_file, 'EC2') - self.assertRaises(ASError, - discover_config_server, self.cloud_info_file, http=HttpUnitTest(), - ec2_user_data='http://169.254.169.254/empty-user-data') - - -class TestAudreyStartupProvidesParameters(unittest.TestCase): - ''' - Class for exercising the parsing of the Provides ParametersConfigs - from the CS. - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - ''' - setup_logging(logging.DEBUG, './test_audrey_agent.log') - - def test_success_parameters(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - ''' - - # Establish valid test data: - src = '|operatingsystem&is_virtual|' - - print '\nTest Name: test_success_parameters()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['operatingsystem', 'is_virtual'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - print 'len(provides): ' + str(len(provides)) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - self.assertTrue('audrey_data=%7Coperatingsystem' in provides) - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - def test_success_no_params(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - Containging an unavailable parameter - ''' - - # Establish valid test data: - src = '|uptime_days&unavailable_dogs&ipaddress|' - - print '\nTest Name: test_success_no_params()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['uptime_days', 'unavailable_dogs', 'ipaddress'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - # Confirm unavailable parameters return an empty string. - self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) - - def test_success_one_parameters(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - with only one parameter - ''' - - # Establish valid test data: - src = '|uptime_days|' - - print '\nTest Name: test_success_parameters()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['uptime_days'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - def test_success_one_parameter(self): - ''' - Success case: - - Exercise parse_provides_params() and generate_provides() - with valid input - - With only one parameter which is unavailable - ''' - - # Establish valid test data: - src = '|unavailable_dogs|' - - print '\nTest Name: test_success_one_parameter()' - print 'Test input:\n' + src - print 'Expect: parse_provides_params() success' - - expected_params_list = ['unavailable_dogs'] - - # Exersise code segment - params_list = parse_provides_params(src) - provides = generate_provides(src) - print 'src: ' + str(src) - print 'params_list: ' + str(params_list) - print 'provides: ' + str(provides) - - # Validate results - self.assertEqual(params_list, expected_params_list) - - # The values are not validatable because they are unpredictable - # but all the expected parameters should be returned. - # Note: %7C is the encoded |, %26 is the encoded & - for param in expected_params_list: - self.assertTrue('%7C' + str(param) in provides) - - # Confirm unavailable parameters return an empty string. - self.assertTrue('%7C' + 'unavailable_dogs' + '%26%7C' in provides) - - def test_failure_missing_delimiter(self): - ''' - Failure case: - - Exercise parse_provides_params() and generate_provides() - with invalid input - - missing leading delimiter - ''' - - # Establish valid test data: - src = 'unavailable_dogs|' - - print '\nTest Name: test_failure_missing_delimiter()' - print 'Test input:\n' + src - print 'Expect: parse_require_config() ASError' - - expected_params_list = ['unavailable_dogs'] - - # Exersise code segment and validate results - with self.assertRaises(ASError): - params_list = parse_provides_params(src) - - with self.assertRaises(ASError): - provides = generate_provides(src) - -class TestConfigServerClient(unittest.TestCase): - ''' - Class for exercising the gets and put to and from the CS - ''' - - def setUp(self): - ''' - If the cloud info file is not present assume running in a - UNITTEST environment. This will allow for exercising some - of the code without having to be running in a cloud VM. - - Set up logging. - ''' - - setup_logging(logging.DEBUG, './test_audrey_agent.log') - - # Create the client Object - self.cs_client = CSClient(**DUMMY_CS_CONFIG) - self.cs_client.http = HttpUnitTest() - - def tearDown(self): - pass - - def test_success_get_cs_configs(self): - ''' - Success case: - - Exercise get_cs_configs() - ''' - print '\n\n--- Test Name: test_success_get_cs_configs ---' - - self.cs_client.get_cs_configs() - - # Add asserts - print 'test_success_get_cs_configs() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - - def test_success_get_cs_tooling(self): - ''' - Success case: - - Exercise get_cs_tooling() - ''' - self.cs_client.get_cs_tooling() - - def test_success_get_cs_params(self): - ''' - Success case: - - Exercise get_cs_params() - ''' - print '\n\n--- Test Name: test_success_get_cs_params ---' - - self.cs_client.get_cs_params() - - # Add asserts - print 'test_success_get_cs_params() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - - def test_success_get_cs_confs_n_params(self): - ''' - Success case: - - Exercise get_cs_configs() and get_cs_params() - ''' - print '\n\n--- Test Name: test_success_get_cs_confs_and_params ---' - - self.cs_client.get_cs_configs() - self.cs_client.get_cs_params() - - # Add asserts - print 'test_success_get_cs_confs_n_params() Add asserts' - print 'self.cs_client : START \n' + str(self.cs_client) + \ - '\nself.cs_client : END' - print 'test_success_get_cs_confs_n_params() Add asserts' - - def test_success_put_cs_params_values(self): - ''' - Success case: - - Exercise put_cs_params_values() - ''' - self.cs_client.put_cs_params_values('') - - def test_error_http_status(self): - ''' - Success case: - - Exercise put_cs_params_values() - ''' - self.assertRaises(ASError, self.cs_client._validate_http_status, 401) - -class TestAudreyScript(unittest.TestCase): - ''' - Class for exercising the full audrey script functionality - ''' - - def setUp(self): - ''' - Perform required setup including setting up logging. - - This test currently require to be run in a cloud VM - with a live Config Server. - ''' - setup_logging(logging.DEBUG, './test_audrey_agent.log') - # make a copy of argv - self.argv = list(sys.argv) - - def tearDown(self): - # replace argv - sys.argv = list(self.argv) - - def test_audrey_script_main(self): - ''' - Perform what the audrey script will do. - ''' - cloud_info_file = 'cloud_info' - sys.argv.extend(['-p']) - _write_info_file(cloud_info_file, 'EC2') - audrey_script_main(HttpUnitTest()) - os.remove(cloud_info_file) - - def test_fail_audrey_script_main(self): - ''' - Perform what the audrey script will do. - ''' - self.assertRaises(ASError, audrey_script_main) - - def test_empty_gen_env(self): - self.assertRaises(ASError, gen_env, '', '') - - # doesn't actually test what I wanted it to. - #def test_parse_require_config(self): - # self.assertRaises(ASError, parse_require_config, '') - -if __name__ == '__main__': - - setup_logging(logging.DEBUG, logfile_name='./test_audrey_agent.log') - unittest.main()
--- configserver/Rakefile | 23 +++++++++++++++++++++++ 1 files changed, 23 insertions(+), 0 deletions(-)
diff --git a/configserver/Rakefile b/configserver/Rakefile index e04a5db..8236bbc 100644 --- a/configserver/Rakefile +++ b/configserver/Rakefile @@ -156,6 +156,29 @@ task :rpm => [ :package ] do |t| end end
+# Build the SRPM +desc "Build SRPM for #{pkg_name}" +task :srpm => [ :package ] do |t| + Dir::chdir("pkg") do |dir| + dir = File::expand_path(".") + system("which rpmbuild &> /dev/null") + if $? != 0 + raise "No rpmbuild command found. Please install the " \ + "rpm-build package to run the rpm rake task." + end + # set pipefail so if the command fails, it's status is reported back + # to the rake task + cmd = "set -o pipefail && rpmbuild " \ + "--define '_sourcedir #{dir}' " \ + "-bs #{spec_path} | tee rpmbuild.log 2>&1" + system(cmd) + cmd_status = $? + if cmd_status != 0 + raise "#{cmd} failed (exit code: #{cmd_status})" + end + end +end + # Default Build all desc "Build (S)RPMs for #{pkg_name}" task :default => [ :rpm ]
aeolus-devel@lists.fedorahosted.org