diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c34cc51 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +- version 0.24 + +Fixed bug with user creation in the context of a space. Adding users with role developer to a space now works. diff --git a/LICENSE.txt b/LICENSE.txt index 27e796d..970c63f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Jose Riguera Lopez +Copyright (c) 2016 Springer Nature Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0e67b14..3d096b5 100644 --- a/README.md +++ b/README.md @@ -68,5 +68,6 @@ print(org) ## Author -Jose Riguera Lopez, jose.riguera@springer.com -SpringerNature Platform Engineering +Springer Nature Platform Engineering, Jose Riguera Lopez (jose.riguera@springer.com) + +Copyright 2017 Springer Nature diff --git a/cf-apps.py b/cf-apps.py new file mode 100755 index 0000000..173abef --- /dev/null +++ b/cf-apps.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Program to query CF apps status based on filters +""" +# Python 2 and 3 compatibility +from __future__ import unicode_literals, print_function + +__program__ = "cf-apps" +__version__ = "0.1" +__author__ = "Jose Riguera" +__year__ = "2017" +__email__ = "" +__license__ = "MIT" +__purpose__ = """ +List information about applications available in Cloud Foundry using `GET /v2/apps` endpoint + +You can define lambda filters with the variable `x` which represents the CF API output, like this: +* "int(x['entity']['instances']) <= 1": will list all apps with 1 or less instances +* "int(x['entity']['instances']) <= 1 and x['entity']['state'] == 'STARTED'": same as before but only the started ones +* "x['entity']['state'] == 'STOPPED'": all apps stopped + +For more information about the filter parameters and fields, see https://apidocs.cloudfoundry.org/ +""" + +from cfconfigurator.cf import CF +import argparse + + + +def run(user, password, api, entity_filter, entity_fields=['name']): + cf = CF(api) + cf.login(user, password) + result = cf.request('GET', "/v2/apps", {"results-per-page": 100}) + apps = result[0] + print("* Total results: %s" % apps['total_results']) + data = apps['resources'] + fun = "filter(lambda x: %s, data)" % entity_filter + filtered = list(eval(fun)) + for entity in filtered: + app = entity['entity'] + fields = [str(app[x]) if x in app else x for x in entity_fields] + #print(app) + print(" ".join(fields)) + print("* Apps: %d" % len(filtered)) + + +def main(): + # Argument parsing + epilog = __purpose__ + '\n' + epilog += __version__ + ', ' + __year__ + ' ' + epilog += __author__ + ' ' + __email__ + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + description=__doc__, epilog=epilog) + parser.add_argument('filter', default="True", nargs='?', help='Entity filter definition for each item') + parser.add_argument('-u', '--user', default="admin", type=str, help='User to query the CF API') + parser.add_argument('-p', '--password', default="admin", type=str, help='Password for the user') + parser.add_argument('-a', '--api', type=str, help='CF API url') + parser.add_argument('-f', '--fields', default="name,is,state,with,instances,instances.", help='Fields and words to show in the output') + + args = parser.parse_args() + fields = args.fields.split(',') + print(fields) + run(args.user, args.password, args.api, args.filter, fields) + + +if __name__ == "__main__": + main() diff --git a/cfconfigurator/__init__.py b/cfconfigurator/__init__.py index cfe09c0..79f65bf 100644 --- a/cfconfigurator/__init__.py +++ b/cfconfigurator/__init__.py @@ -5,8 +5,8 @@ """ __program__ = "cfconfigurator" -__version__ = "0.2.0" +__version__ = "0.2.5" __author__ = "Jose Riguera" -__year__ = "2016" +__year__ = "2017" __email__ = "" __license__ = "MIT" diff --git a/cfconfigurator/cf.py b/cfconfigurator/cf.py index 68488dc..04c14e3 100644 --- a/cfconfigurator/cf.py +++ b/cfconfigurator/cf.py @@ -135,9 +135,35 @@ def _request(self, method, url, params=None, http_headers=None, data=None): raise CFException(error, resp.status_code) return response, resp.status_code + def request(self, method, url, params=None, http_headers=None, data=None): + api = self.api_url + if url.startswith('/'): + url = api + url + else: + parsed = requests.compat.urlparse(url) + if parsed.scheme == '' or parsed.netloc == '': + raise ValueError("url not valid") + api = parsed.scheme + "://" + parsed.netloc + response, rcode = self._request(method, url, params, http_headers, data) + if 'resources' in response and 'total_pages' in response: + pages = response['total_pages'] + pages_counter = 1 + while response['next_url'] != None and rcode == 200: + part_url = api + response['next_url'] + part_resp, rcode = self._request(method, part_url, None, http_headers, data) + pages_counter += 1 + part_resp['resources'] = response['resources'] + part_resp['resources'] + response = part_resp + if pages_counter != pages: + msg = "number of expected pages different than actual pages" + error = {'description': "Pagination error " + msg} + raise CFException(error, rcode) + response['next_url'] = None + response['prev_url'] = None + return response, rcode def _get(self, url, params=None): - resp, rcode = self._request('GET', url, params) + resp, rcode = self.request('GET', url, params) if rcode != 200: raise CFException(resp, rcode) return resp @@ -150,14 +176,14 @@ def _search(self, url, params): return resp['resources'][0] def _delete(self, url, params=None): - resp, rcode = self._request('DELETE', url, params) + resp, rcode = self.request('DELETE', url, params) if rcode != 204: raise CFException(resp, rcode) def _update(self, create, url, data=None): method = 'POST' if create else 'PUT' json_data = None if data is None else json.dumps(data) - resp, rcode = self._request(method, url, None, None, json_data) + resp, rcode = self.request(method, url, None, None, json_data) if rcode != 201 and rcode != 200: raise CFException(resp, rcode) return resp @@ -166,7 +192,7 @@ def _update(self, create, url, data=None): def clean_blobstore_cache(self): """Deletes all of the existing buildpack caches in the blobstore""" url = self.api_url + self.blobstores_builpack_cache_url - resp, rcode = self._request('DELETE', url) + resp, rcode = self.request('DELETE', url) if rcode != 202: raise CFException(resp, rcode) return resp @@ -200,7 +226,7 @@ def manage_variable_group(self, key, value='', name="running", add=True): pass if changed: json_data = json.dumps(variables) - resp, rcode = self._request('PUT', url, None, None, json_data) + resp, rcode = self.request('PUT', url, None, None, json_data) if rcode != 200: raise CFException(resp, rcode) return changed @@ -217,7 +243,7 @@ def manage_feature_flags(self, name, enabled): data = { 'enabled': enabled } - resp, rcode = self._request('PUT', url, None, None, json.dumps(data)) + resp, rcode = self.request('PUT', url, None, None, json.dumps(data)) if rcode != 200: raise CFException(resp, rcode) return True @@ -515,11 +541,26 @@ def save_user(self, name, givenName, familyName, email=None, password=None, user = self.uaa.user_get(user_id) changed = not ( name == user['userName'] and - familyName == user['name']['familyName'] and - givenName == user['name']['givenName'] and active == user['active'] and origin == user['origin'] ) + # Users should have user['name']['familyName'] and + # user['name']['givenName'] but there are some special cases + # (admin, doppler, etc) without those fields + names_list = [] + if 'name' in user: + if 'givenName' in user['name']: + surname = user['name']['givenName'] + if givenName != user['name']['givenName']: + changed = True + surname = givenName + names_list.append(surname) + if 'familyName' in user['name']: + surname = user['name']['familyName'] + if familyName != user['name']['familyName']: + changed = True + surname = familyName + names_list.append(surname) if externalId is not None: if 'externalId' not in user: changed = True @@ -532,23 +573,24 @@ def save_user(self, name, givenName, familyName, email=None, password=None, email_list = [e['value'] for e in user['emails']] if email not in email_list: changed = True - email_list.append(email) + email_list.insert(0, email) else: # it allows other emails email_list = [e['value'] for e in user['emails']] if changed: self.uaa.user_save( - name, [givenName, familyName], password, email_list, + name, names_list, password, email_list, active=active, origin=origin, externalId=externalId, id=user_id) if force_pass: # Special UAA privs are required to change passwords! self.uaa.user_set_password(user_id, password) else: + names_list = [givenName, familyName] changed = True email_list = [] if email is None else [email] user = self.uaa.user_save( - name, [givenName, familyName], password, email_list, + name, names_list, password, email_list, active=active, origin=origin, externalId=externalId) user_id = user['id'] except UAAException as e: @@ -616,14 +658,14 @@ def manage_organization_users(self, orguid, userid, role='user', add=True): def manage_space_users(self, spuid, userid, role='user', add=True): url = self.api_url - if role == 'user': + if role == 'developer': url += self.users_spaces_url elif role == 'manager': url += self.users_managed_spaces_url elif role == 'auditor': url += self.users_audited_spaces_url else: - raise ValueError("Invalid role, options: user, manager, auditor") + raise ValueError("Invalid role, options: developer, manager, auditor") url = url % userid found = False resp = self._get(url) diff --git a/cfconfigurator/uaa.py b/cfconfigurator/uaa.py index 6fd4bad..81fa387 100644 --- a/cfconfigurator/uaa.py +++ b/cfconfigurator/uaa.py @@ -214,23 +214,23 @@ def user_get(self, id, version='*'): resp, rc = self._request('GET', url, http_headers=headers) return resp - def user_save(self, name, usernames, password='', emails=[], active=True, + def user_save(self, name, usernames=[], password='', emails=[], active=True, verified=True, phones=[], origin='uaa', externalId='', id=None, version='*'): url = self.api_url + self.user_url data = { 'userName': name, 'origin': origin, - 'name': { - 'formatted': " ".join(usernames), - 'familyName': usernames[-1], - 'givenName': usernames[0], - }, + 'name': {}, 'emails': [], 'phoneNumbers': [], 'active': active, 'verified': verified, } + if usernames: + data['name']['formatted'] = " ".join(usernames) + data['name']['givenName'] = usernames[0] + data['name']['familyName'] = usernames[-1] if emails: for i, e in enumerate(emails): email = { diff --git a/example.py b/example.py index ead4e78..2a24f18 100644 --- a/example.py +++ b/example.py @@ -5,10 +5,17 @@ api_url = "https://api.test.cf.springer-sbm.com" admin_user = "admin" -admin_password = "admin" +admin_password = "password" cf = CF(api_url) cf.login(admin_user, admin_password) org = cf.search_org("pivotal") print(org) + +services = cf.request('GET', "/v2/services", {"results-per-page": 1}) +print(services) + +services = cf.request('GET', "https://api.test.cf.springer-sbm.com/v2/services", {"results-per-page": 1}) +print(services) + diff --git a/example2.py b/example2.py new file mode 100644 index 0000000..fcbe4ca --- /dev/null +++ b/example2.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +python-cfconfigurator is a simple and small library to manage Cloud Foundry +(c) 2016 Jose Riguera Lopez, jose.riguera@springer.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" +# Python 2 and 3 compatibility +from __future__ import unicode_literals, print_function + +from cfconfigurator.exceptions import CFException, UAAException +from cfconfigurator.cf import UAA +from cfconfigurator.cf import CF + + +def main(): + u = UAA("https://uaa.test.example.com", "admin", "admin-secret") + a = u.login() + print(a) + + user = u.user_find({'userName': 'jose'}) + if user['totalResults'] == 1: + deleted = u.user_delete(user['resources'][0]['id']) + print(deleted) + new_user = u.user_save("jose", ["Jose", "Riguera"], "hola", ["jriguera@hola.com"]) + print(new_user) + user = u.user_get(new_user['id']) + pas = u.user_set_password(user['id'], 'adios') + print(pas) + + print("=====================") + + group = u.group_find({'displayName': 'josegroup'}) + if group['totalResults'] == 1: + deleted = u.group_delete(group['resources'][0]['id']) + print(deleted) + new_group = u.group_save("josegroup", "Jose Riguera Group") + print(new_group) + # add user + group_member = u.group_manage_member(new_group['id'], user['id']) + print(group_member) + group = u.group_get(group_member['id']) + print(group) + # remove user + group_member = u.group_manage_member(group['id'], user['id'], add=False) + print(group_member) + + print("=====================") + + clients = u.client_find({'client_id': 'joseclient'}) + print(clients) + if clients['totalResults'] == 1: + deleted = u.client_delete(clients['resources'][0]['client_id']) + print(deleted) + new_client = u.client_save("joseclient", "Jose Riguera client", "hola", "adios", scope=['uaa.user']) + print(new_client) + client_secret = u.client_set_secret(new_client['client_id'], "pedro") + print(client_secret) + client = u.client_save("joseclient", "JoseRRRRRRRRRRRR", "hola", "token", scope=['uaa.user'], id=new_client['client_id']) + print(client) + clients = u.client_find({'client_id': 'joseclient'}) + print(clients) + + # delete + u.group_delete(group['id']) + u.user_delete(user['id']) + u.client_delete(client['client_id']) + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt index d373a14..9c558e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests>=2.10.0 +. diff --git a/setup.py b/setup.py index bbf6e52..a3e4256 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ """ Setuptools module for python-cfconfigurator See: - https://packaging.python.org/en/latest/distributing.html + https://packaging.python.org/en/latest/distributing.html """ # Always prefer setuptools over distutils @@ -11,10 +11,12 @@ # To use a consistent encoding from codecs import open from os import path -from pip.download import PipSession -from pip.req import parse_requirements import re +requirements=[ + "requests>=2.10.0", +] + def find_version(*file_paths): # Open in Latin-1 so that we avoid encoding errors. @@ -24,8 +26,7 @@ def find_version(*file_paths): version_file = f.read() # The version line must have the form # __version__ = 'ver - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") @@ -36,17 +37,10 @@ def find_readme(f="README.md"): # Get the long description from the README file long_description = None with open(path.join(here, f), encoding='utf-8') as f: - long_description = f.read() + long_description = f.read() return long_description -def find_requirements(f='requirements.txt'): - # parse_requirements() returns generator of pip.req.InstallRequirement objects - reqs = parse_requirements("requirements.txt", session=PipSession()) - install_reqs = [str(ir.req) for ir in reqs] - return install_reqs - - setup( name="cfconfigurator", url="https://github.com/SpringerPE/python-cfconfigurator", @@ -85,5 +79,5 @@ def find_requirements(f='requirements.txt'): ], # Dependent packages (distributions) - install_requires=find_requirements(), + install_requires=requirements, )