Skip to content

Commit

Permalink
Merge pull request #276 from agccie/appcenter_subscription
Browse files Browse the repository at this point in the history
add subscription support for appcenter_user with cert authentication
  • Loading branch information
michsmit99 committed Jan 18, 2017
2 parents dfd453f + 2e7269b commit f0a8f9f
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 13 deletions.
53 changes: 42 additions & 11 deletions acitoolkit/acisession.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,17 @@ def has_events(self, url):
result = len(self._events[url]) != 0
return result

def get_event_count(self, url):
"""
Check the number of subscription events for a particular APIC URL
:param url: URL string to check for pending events
:returns: Interger number of events in event queue
"""
self._process_event_q()
if url not in self._events: return 0
return len(self._events[url])

def get_event(self, url):
"""
Get an event for a particular APIC URL subscription.
Expand Down Expand Up @@ -488,8 +499,9 @@ def __init__(self, url, uid, pwd=None, cert_name=None, key=None, verify_ssl=Fals
Please install it using "pip install pyopenssl"')

self.cert_auth = True
# Cert based auth does not support subscriptions :(
if subscription_enabled:
# Cert based auth does not support subscriptions :(
# there's an exception for appcenter_user relying on the requestAppToken api
if subscription_enabled and not self.appcenter_user:
logging.warning('Disabling subscription support as certificate authentication does not support it.')
logging.warning('Consider passing subscription_enabled=False to hide this warning message.')
subscription_enabled = False
Expand Down Expand Up @@ -548,6 +560,11 @@ def _prep_x509_header(self, method, url, data=None):
if not self.cert_auth:
return {}

# for appcenter_user with subscription enabled and currently logged_in
# no need to build x509 header since authentication is using token
if self.appcenter_user and self._subscription_enabled and self._logged_in:
return {}

if not self.session:
self.session = requests.Session()

Expand Down Expand Up @@ -595,17 +612,21 @@ def _send_login(self, timeout=None):
except AttributeError:
pass
self.session = requests.Session()
self._logged_in = False

if self.cert_auth:
if self.appcenter_user and self._subscription_enabled:
login_url = '/api/requestAppToken.json'
data = {'aaaAppToken':{'attributes':{'appName': self.cert_name}}}
elif self.cert_auth:
logging.warning('Will not explicitly login because certificate based authentication is being used for this session.')
logging.warning('If permanently using cert auth, consider removing the call to login().')
CertAuthResponse = namedtuple('CertAuthResponse', ['ok'])
return CertAuthResponse(ok=True)

login_url = '/api/aaaLogin.json'
name_pwd = {'aaaUser': {'attributes': {'name': self.uid,
'pwd': self.pwd}}}
ret = self.push_to_apic(login_url, data=name_pwd, timeout=timeout)
else:
login_url = '/api/aaaLogin.json'
data = {'aaaUser': {'attributes': {'name': self.uid,
'pwd': self.pwd}}}
ret = self.push_to_apic(login_url, data=data, timeout=timeout)
if not ret.ok:
logging.error('Could not relogin to APIC. Aborting login thread.')
self.login_thread.exit()
Expand Down Expand Up @@ -637,7 +658,7 @@ def login(self, timeout=None):
resp = requests.Response()
resp.status_code = 404
resp._content = '{"error": "Could not relogin to APIC due to ConnectionError"}'
if not self.cert_auth:
if (self.appcenter_user and self._subscription_enabled) or not self.cert_auth:
self.login_thread.daemon = True
self.login_thread.start()
return resp
Expand Down Expand Up @@ -709,6 +730,15 @@ class and instance subscriptions.
"""
return self.subscription_thread.has_events(url)

def get_event_count(self, url):
"""
Check the number of subscription events for a particular APIC URL
:param url: URL string belonging to subscription
:returns: Interger number of events in event queue
"""
return self.subscription_thread.get_event_count(url)

def get_event(self, url):
"""
Get an event for a particular URL. Used internally by the
Expand Down Expand Up @@ -744,7 +774,8 @@ def push_to_apic(self, url, data, timeout=None):
post_url = self.api + url
logging.debug('Posting url: %s data: %s', post_url, data)

if self.cert_auth:
if self.cert_auth and not \
(self.appcenter_user and self._subscription_enabled and self._logged_in):
data = json.dumps(data, sort_keys=True)
cookies = self._prep_x509_header('POST', url, data)
resp = self.session.post(post_url, data=data, verify=self.verify_ssl,
Expand Down Expand Up @@ -783,7 +814,7 @@ def get(self, url, timeout=None):
cookies = self._prep_x509_header('GET', url)
resp = self.session.get(get_url, timeout=timeout, verify=self.verify_ssl, proxies=self._proxies, cookies=cookies)
if resp.status_code == 403:
if self.cert_auth:
if self.cert_auth and not (self.appcenter_user and self._subscription_enabled):
logging.error('Certificate authentication failed. Please check all settings are correct.')
resp.raise_for_status()
else:
Expand Down
8 changes: 6 additions & 2 deletions docs/source/tutorialsimpleconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,14 @@ installed using pip
.. note:: If using the acitoolkit from the context of an APIC App Center app, make sure to pass the extra
parameter ``appcenter_user=True``. App Center apps are provided a user that belongs to a different class
of users.
of users. The login and cert_name for App Center users are both in the form of ``vendor_appId``.
App Center users support certificate subsciptions through a special requestAppToken api. To use
subscriptions with an App Center user, you must explicitly call the ``login()`` method which acquires
and maintains the App user token. Disable App center subscriptions by setting the parameter
``subscription_enabled=False``.


You do not need to explicitly call the ``login()`` method when using certificate authentication.
You do not need to explicitly call the ``login()`` method when using certificate authentication.

After this point, you can continue to use all of the acitoolkit methods to get and push configuration from the APIC securely and without logging in.

Expand Down
74 changes: 74 additions & 0 deletions tests/acitoolkit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@
CERT_NAME = ''
KEY = ''
""")
try:
from credentials import APPCENTER_LOGIN, APPCENTER_CERT_NAME, APPCENTER_KEY
except ImportError:
print('To run appcenter tests, please create a credentials.py file with the following variables filled in:')
print("""
APPCENTER_LOGIN=''
APPCENTER_CERT_NAME=''
APPCENTER_KEY=''
""")


MAX_RANDOM_STRING_SIZE = 20

Expand Down Expand Up @@ -3219,6 +3229,69 @@ def test_get_with_params(self):
)
self.assertTrue(len(tenants) > 0)

class TestLiveAppcenterSubscription(unittest.TestCase):
"""
Certificate subscription tests with a live APIC
Note, this test requires appcenter user credentials and valid appcenter user private key
"""

def login_to_apic(self):
"""Login to the APIC using Certificate auth with appcenter_user enabled
RETURNS: Instance of class Session
"""
session = Session(URL, APPCENTER_LOGIN, cert_name=APPCENTER_CERT_NAME,
key=APPCENTER_KEY, subscription_enabled=True, appcenter_user=True)
resp = session.login()
self.assertTrue(resp.ok)
return session

def test_get_actual_event(self):
"""
Test get_event for certificate based subscription
"""
session = self.login_to_apic()
Tenant.subscribe(session)

# Get all of the existing tenants
tenants = Tenant.get(session)
tenant_names = []
for tenant in tenants:
tenant_names.append(tenant.name)

# Pick a unique tenant name not currently in APIC
tenant_name = tenant_names[0]
while tenant_name in tenant_names:
tenant_name = random_size_string()

# Create the tenant and push to APIC
new_tenant = Tenant(tenant_name)
resp = session.push_to_apic(new_tenant.get_url(),
data=new_tenant.get_json())
self.assertTrue(resp.ok)

# Wait for the event to come through the subscription
# If it takes more than 2 seconds, fail the test.
# Pass the test as quickly as possible
start_time = time.time()
while True:
current_time = time.time()
time_elapsed = current_time - start_time
self.assertTrue(time_elapsed < 2)
if Tenant.has_events(session):
break

event_tenant = Tenant.get_event(session)
is_tenant = isinstance(event_tenant, Tenant)
self.assertTrue(is_tenant)

new_tenant.mark_as_deleted()
resp = session.push_to_apic(new_tenant.get_url(),
data=new_tenant.get_json())
self.assertTrue(resp.ok)
Tenant.unsubscribe(session)




class TestLiveTenant(TestLiveAPIC):
"""
Expand Down Expand Up @@ -5287,6 +5360,7 @@ def test_get_unhealthy(self):
live.addTest(unittest.makeSuite(TestLiveTenant))
live.addTest(unittest.makeSuite(TestLiveAPIC))
live.addTest(unittest.makeSuite(TestLiveCertAuth))
live.addTest(unittest.makeSuite(TestLiveAppcenterSubscription))
live.addTest(unittest.makeSuite(TestLiveInterface))
live.addTest(unittest.makeSuite(TestLivePortChannel))
live.addTest(unittest.makeSuite(TestLiveAppProfile))
Expand Down

0 comments on commit f0a8f9f

Please sign in to comment.