---
.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-%{version}....
+
+# 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(a)redhat.com> 0.4.0-12
+- Fixed Auto help file generation induced build failure
+* Thu Nov 17 2011 Joe VLcek <jvlcek(a)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(a)redhat.com> 0.4.0-10
+- Fix bz754769 fix user data parsing and log level
+* Tue Nov 09 2011 Joe VLcek <jvlcek(a)redhat.com> 0.4.0-9
+- Update the licensing information
+* Tue Nov 08 2011 Joe VLcek <jvlcek(a)redhat.com> 0.4.0-8
+- Add man page generation and address some packaging review feedback
+* Tue Nov 08 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-7
+- Fixing changelog history
+* Mon Nov 07 2011 Joe VLcek <jvlcek(a)redhat.com> 0.3.1-7
+- Address Packaging for Fedora review feedback
+* Fri Nov 05 2011 Joe VLcek <jvlcek(a)redhat.com> 0.3.1-6
+- Packaging for Fedora
+* Wed Nov 02 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-6
+- Handles base64 encoded and decoded data in user data
+* Wed Nov 02 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-3
+- Fix for audrey.log location
+* Thu Oct 27 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-2
+- Radez doubled unit test coverage
+* Wed Oct 26 2011 Greg Blomquist <gblomqui(a)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(a)redhat.com> 0.3.1-3
+- adding requires for rpm installations
+* Tue Oct 11 2011 Joe VLcek <joev(a)redhat.com> 0.3.1-2
+- RHEVm user data injection base64 encoded.
+* Fri Sep 30 2011 Joe VLcek <joev(a)redhat.com> 0.3.1-1
+- RHEVm user data injection.
+* Wed May 18 2011 Joe VLcek <joev(a)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/|oauthConsumer|oauthSecret'
+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-%{version}....
-
-# 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(a)redhat.com> 0.4.0-12
-- Fixed Auto help file generation induced build failure
-* Thu Nov 17 2011 Joe VLcek <jvlcek(a)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(a)redhat.com> 0.4.0-10
-- Fix bz754769 fix user data parsing and log level
-* Tue Nov 09 2011 Joe VLcek <jvlcek(a)redhat.com> 0.4.0-9
-- Update the licensing information
-* Tue Nov 08 2011 Joe VLcek <jvlcek(a)redhat.com> 0.4.0-8
-- Add man page generation and address some packaging review feedback
-* Tue Nov 08 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-7
-- Fixing changelog history
-* Mon Nov 07 2011 Joe VLcek <jvlcek(a)redhat.com> 0.3.1-7
-- Address Packaging for Fedora review feedback
-* Fri Nov 05 2011 Joe VLcek <jvlcek(a)redhat.com> 0.3.1-6
-- Packaging for Fedora
-* Wed Nov 02 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-6
-- Handles base64 encoded and decoded data in user data
-* Wed Nov 02 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-3
-- Fix for audrey.log location
-* Thu Oct 27 2011 Greg Blomquist <gblomqui(a)redhat.com> 0.4.0-2
-- Radez doubled unit test coverage
-* Wed Oct 26 2011 Greg Blomquist <gblomqui(a)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(a)redhat.com> 0.3.1-3
-- adding requires for rpm installations
-* Tue Oct 11 2011 Joe VLcek <joev(a)redhat.com> 0.3.1-2
-- RHEVm user data injection base64 encoded.
-* Fri Sep 30 2011 Joe VLcek <joev(a)redhat.com> 0.3.1-1
-- RHEVm user data injection.
-* Wed May 18 2011 Joe VLcek <joev(a)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/|oauthConsumer|oauthSecret'
-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()
--
1.7.7.3