diff --git a/README.md b/README.md index 9431d0c..7d42408 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ This repository also includes tools to build PAPI bindings yourself for a large ```sh python setup.py install --user ``` -(or `sudo python setup.py install` to install the package for all users) You may need to install the Python [Setuptools](http://pypi.python.org/pypi/setuptools) on your system, if they are not already installed. For instructions, see http://pypi.python.org/pypi/setuptools. diff --git a/components/create_swagger_config.py b/components/create_swagger_config.py index f948977..7bb7df7 100755 --- a/components/create_swagger_config.py +++ b/components/create_swagger_config.py @@ -12,6 +12,7 @@ import re import requests from requests.auth import HTTPBasicAuth +import traceback import sys requests.packages.urllib3.disable_warnings() @@ -111,7 +112,8 @@ def FindBestTypeForProp(prop): def IsiArrayPropToSwaggerArrayProp( prop, propName, - isiObjName, isiObjNameSpace, isiSchemaProps, objDefs, classExtPostFix): + isiObjName, isiObjNameSpace, isiSchemaProps, objDefs, + classExtPostFix, isResponseObject): if "items" not in prop and "item" in prop: prop["items"] = prop["item"] @@ -138,7 +140,7 @@ def IsiArrayPropToSwaggerArrayProp( objRef = IsiSchemaToSwaggerObjectDefs( itemsObjNameSpace, itemsObjName, prop["items"], objDefs, - classExtPostFix) + classExtPostFix, isResponseObject) isiSchemaProps[propName]["items"] = \ {"description" : propDescription, "$ref" : objRef} elif "type" in prop["items"] \ @@ -160,7 +162,7 @@ def IsiArrayPropToSwaggerArrayProp( objRef = IsiSchemaToSwaggerObjectDefs( itemsObjNameSpace, itemsObjName, prop["items"]["type"], objDefs, - classExtPostFix) + classExtPostFix, isResponseObject) isiSchemaProps[propName]["items"] = {"$ref" : objRef} elif "type" in prop["items"] \ and type(prop["items"]["type"]) == list: @@ -170,7 +172,7 @@ def IsiArrayPropToSwaggerArrayProp( and prop["items"]["type"] == "array": IsiArrayPropToSwaggerArrayProp(prop["items"], "items", isiObjName, isiObjNameSpace, isiSchemaProps[propName], - objDefs, classExtPostFix) + objDefs, classExtPostFix, isResponseObject) elif "type" in prop["items"]: if prop["items"]["type"] == "any" or prop["items"]["type"] == "string": # Swagger does not support "any" @@ -195,7 +197,7 @@ def IsiArrayPropToSwaggerArrayProp( def IsiSchemaToSwaggerObjectDefs( isiObjNameSpace, isiObjName, isiSchema, objDefs, - classExtPostFix="Extended"): + classExtPostFix, isResponseObject=False): # converts isiSchema to a single schema with "#ref" for sub-objects # which is what Swagger expects. Adds the sub-objects to the objDefs # list. @@ -253,7 +255,18 @@ def IsiSchemaToSwaggerObjectDefs( continue # must be a $ref if "required" in prop: if prop["required"] == True: - requiredProps.append(propName) + # Often the PAPI will have a required field whose value can be + # either a real value, such as a string, or it can be a null, + # which Swagger can not deal with. This is only problematic + # in objects that are returned as a response because it ends up + # causing the Swagger code to throw an exception upon receiving + # a PAPI response that contains null values for the "required" + # fields. So if the type is a multi-type (i.e. list) and + # isResponseObject is True, then we don't add the field to the + # list of required fields. + if type(prop["type"]) != list \ + or isResponseObject is False: + requiredProps.append(propName) del prop["required"] if type(prop["type"]) == list: @@ -274,7 +287,7 @@ def IsiSchemaToSwaggerObjectDefs( objRef = IsiSchemaToSwaggerObjectDefs( subObjNameSpace, subObjName, prop, objDefs, - classExtPostFix) + classExtPostFix, isResponseObject) isiSchema["properties"][propName] = \ {"description" : propDescription, "$ref" : objRef} elif type(prop["type"]) == dict \ @@ -290,13 +303,13 @@ def IsiSchemaToSwaggerObjectDefs( objRef = IsiSchemaToSwaggerObjectDefs( subObjNameSpace, subObjName, prop["type"], objDefs, - classExtPostFix) + classExtPostFix, isResponseObject) isiSchema["properties"][propName] = \ {"description" : propDescription, "$ref" : objRef} elif prop["type"] == "array": IsiArrayPropToSwaggerArrayProp(prop, propName, isiObjName, isiObjNameSpace, isiSchema["properties"], - objDefs, classExtPostFix) + objDefs, classExtPostFix, isResponseObject) # code below is work around for bug in /auth/access/ end point elif prop["type"] == "string" and "enum" in prop: newEnum = [] @@ -338,24 +351,56 @@ def IsiSchemaToSwaggerObjectDefs( # attache required props if len(requiredProps) > 0: isiSchema["required"] = requiredProps + elif "required" in isiSchema: + # sometimes top-level objects have this field even though it is + # redundant + del isiSchema["required"] return FindOrAddObjDef(objDefs, isiSchema, isiObjNameSpace + isiObjName, classExtPostFix) +def GetObjectDef(objName, objDefs): + curObj = objDefs[objName] + if "allOf" in curObj: + refObjName = os.path.basename(curObj["allOf"][0]["$ref"]) + refObj = GetObjectDef(refObjName, objDefs) + + fullObjDef = {} + fullObjDef["properties"] = curObj["allOf"][-1]["properties"].copy() + fullObjDef["properties"].update(refObj["properties"]) + if "required" in curObj["allOf"][-1]: + fullObjDef["required"] = list(curObj["allOf"][-1]["required"]) + if "required" in refObj: + try: + fullObjDef["required"].extend(refObj["required"]) + # eliminate dups + fullObjDef["required"] = list(set(fullObjDef["required"])) + except KeyError: + fullObjDef["required"] = list(refObj["required"]) + return fullObjDef + return curObj + + def FindOrAddObjDef(objDefs, newObjDef, newObjName, classExtPostFix): """ Reuse existing object def if there's a match or add a new one Return the "definitions" path """ + extendedObjName = newObjName for objName in objDefs: - existingObjDef = objDefs[objName] - if "allOf" in existingObjDef: - continue #skip subclasses + existingObjDef = GetObjectDef(objName, objDefs) if newObjDef["properties"] == existingObjDef["properties"]: - return "#/definitions/" + objName + if sorted(newObjDef.get("required", [])) == \ + sorted(existingObjDef.get("required", [])): + return "#/definitions/" + objName + else: + # the only difference is the list of required props, so use + # the existingObjDef as the basis for an extended object. + extendedObjName = objName + break - if newObjName in objDefs: + if extendedObjName in objDefs: # TODO at this point the subclass mechanism depends on the data models # being generated in the correct order, where base classes are # generated before sub classes. This is done by processing the @@ -364,30 +409,40 @@ def FindOrAddObjDef(objDefs, newObjDef, newObjName, classExtPostFix): # the same pattern that nfs exports uses is not repeated by the other # endpoints. # crude/limited subclass generation - existingObj = objDefs[newObjName] - if "allOf" in existingObj: - existingProps = existingObj["allOf"][-1]["properties"] - else: - existingProps = objDefs[newObjName]["properties"] + existingObj = GetObjectDef(extendedObjName, objDefs) isExtension = True - for propName in existingProps: + existingProps = existingObj["properties"] + existingRequired = existingObj.get("required", []) + + for propName, propValue in existingProps.iteritems(): if propName not in newObjDef["properties"]: isExtension = False break + elif newObjDef["properties"][propName] != propValue: + isExtension = False + break + if propName in existingRequired \ + and propName not in newObjDef.get("required", []): + isExtension = False + break if isExtension is True: extendedObjDef = \ - { "allOf" : [ { "$ref": "#/definitions/" + newObjName } ] } + { "allOf" : [ { "$ref": "#/definitions/" + extendedObjName } ] } uniqueProps = {} + uniqueRequired = newObjDef.get("required", []) for propName in newObjDef["properties"]: # delete properties that are shared. if propName not in existingProps: uniqueProps[propName] = newObjDef["properties"][propName] + if propName in existingRequired: + uniqueRequired.remove(propName) newObjDef["properties"] = uniqueProps extendedObjDef["allOf"].append(newObjDef) else: extendedObjDef = newObjDef - newObjName += classExtPostFix + while newObjName in objDefs: + newObjName += classExtPostFix objDefs[newObjName] = extendedObjDef else: objDefs[newObjName] = newObjDef @@ -537,7 +592,8 @@ def CreateSwaggerOperation( if isiRespSchema is not None: objRef = IsiSchemaToSwaggerObjectDefs( isiRespObjNameSpace, isiRespObjName, isiRespSchema, - objDefs, classExtPostFix) + objDefs, classExtPostFix, + isResponseObject=True) # create 200 response swagger200Resp = {} swagger200Resp["description"] = isiInputArgs["description"] @@ -999,8 +1055,7 @@ def main(): else: excludeEndPoints = [] endPointPaths = [ - ("/3/network/groupnets//subnets//pools", - "/3/network/groupnets//subnets//pools/")] + ("/1/quota/quotas", "/1/quota/quotas/")] successCount = 0 failCount = 0 @@ -1051,8 +1106,9 @@ def main(): successCount += 1 except Exception as e: + print >> sys.stderr, "Caught exception processing: " + itemEndPointPath if args.test: - print >> sys.stderr, "Caught exception processing: " + itemEndPointPath + traceback.print_exc(file=sys.stderr) failCount += 1 if baseEndPointPath is not None: @@ -1097,8 +1153,9 @@ def main(): print >> sys.stderr, "WARNING: HEAD_args in: " + baseEndPointPath successCount += 1 except Exception as e: + print >> sys.stderr, "Caught exception processing: " + baseEndPointPath if args.test: - print >> sys.stderr, "Caught exception processing: " + baseEndPointPath + traceback.print_exc(file=sys.stderr) failCount += 1 print >> sys.stderr, "End points successfully processed: " + str(successCount) \ diff --git a/swagger-codegen-config.json b/swagger-codegen-config.json index 3f79c06..1061589 100644 --- a/swagger-codegen-config.json +++ b/swagger-codegen-config.json @@ -1,3 +1,4 @@ { - "packageName": "isi_sdk" + "packageName": "isi_sdk", + "packageVersion": "1.0.1" } diff --git a/swagger_templates/model.mustache b/swagger_templates/model.mustache new file mode 100644 index 0000000..70cdc56 --- /dev/null +++ b/swagger_templates/model.mustache @@ -0,0 +1,198 @@ +# coding: utf-8 + +""" +Copyright 2016 SmartBear Software + + 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. + + Ref: https://github.com/swagger-api/swagger-codegen +""" + +{{#models}} +{{#model}} +from pprint import pformat +from six import iteritems +import re + + +class {{classname}}(object): + """ + NOTE: This class is auto generated by the swagger code generator program. + Do not edit the class manually. + """ + def __init__(self): + """ + {{classname}} - a model defined in Swagger + + :param dict swaggerTypes: The key is attribute name + and the value is attribute type. + :param dict attributeMap: The key is attribute name + and the value is json key in definition. + """ + self.swagger_types = { + {{#vars}}'{{name}}': '{{{datatype}}}'{{#hasMore}}, + {{/hasMore}}{{/vars}} + } + + self.attribute_map = { + {{#vars}}'{{name}}': '{{baseName}}'{{#hasMore}}, + {{/hasMore}}{{/vars}} + } + +{{#vars}} + self._{{name}} = {{#defaultValue}}{{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}None{{/defaultValue}} +{{/vars}} + +{{#vars}} + @property + def {{name}}(self): + """ + Gets the {{name}} of this {{classname}}. +{{#description}} {{{description}}}{{/description}} + + :return: The {{name}} of this {{classname}}. + :rtype: {{datatype}} + """ + return self._{{name}} + + @{{name}}.setter + def {{name}}(self, {{name}}): + """ + Sets the {{name}} of this {{classname}}. +{{#description}} {{{description}}}{{/description}} + + :param {{name}}: The {{name}} of this {{classname}}. + :type: {{datatype}} + """ + {{#isEnum}}allowed_values = [{{#allowableValues}}{{#values}}"{{{this}}}"{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}] +{{#required}} + if {{name}} not in allowed_values: +{{/required}} +{{^required}} + if {{name}} is not None and {{name}} not in allowed_values: +{{/required}} + raise ValueError( + "Invalid value for `{{name}}`, must be one of {0}" + .format(allowed_values) + ) +{{/isEnum}} +{{^isEnum}} +{{#hasValidation}} + +{{#required}} + if not {{name}}: + raise ValueError("Invalid value for `{{name}}`, must not be `None`") +{{/required}} +{{#maxLength}} +{{#required}} + if len({{name}}) > {{maxLength}}: +{{/required}} +{{^required}} + if {{name}} is not None and len({{name}}) > {{maxLength}}: +{{/required}} + raise ValueError("Invalid value for `{{name}}`, length must be less than `{{maxLength}}`") +{{/maxLength}} +{{#minLength}} +{{#required}} + if len({{name}}) < {{minLength}}: +{{/required}} +{{^required}} + if {{name}} is not None and len({{name}}) < {{minLength}}: +{{/required}} + raise ValueError("Invalid value for `{{name}}`, length must be greater than or equal to `{{minLength}}`") +{{/minLength}} +{{#maximum}} +{{#required}} + if {{name}} > {{maximum}}: +{{/required}} +{{^required}} + if {{name}} is not None and {{name}} > {{maximum}}: +{{/required}} + raise ValueError("Invalid value for `{{name}}`, must be a value less than or equal to `{{maximum}}`") +{{/maximum}} +{{#minimum}} +{{#required}} + if {{name}} < {{minimum}}: +{{/required}} +{{^required}} + if {{name}} is not None and {{name}} < {{minimum}}: +{{/required}} + raise ValueError("Invalid value for `{{name}}`, must be a value greater than or equal to `{{minimum}}`") +{{/minimum}} +{{#pattern}} +{{#required}} + if not re.search('{{vendorExtensions.x-regex}}', {{name}}{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): +{{/required}} +{{^required}} + if {{name}} is not None and not re.search('{{vendorExtensions.x-regex}}', {{name}}{{#vendorExtensions.x-modifiers}}{{#-first}}, flags={{/-first}}re.{{.}}{{^-last}} | {{/-last}}{{/vendorExtensions.x-modifiers}}): +{{/required}} + raise ValueError("Invalid value for `{{name}}`, must be a follow pattern or equal to `{{pattern}}`") +{{/pattern}} +{{/hasValidation}} +{{/isEnum}} + + self._{{name}} = {{name}} + +{{/vars}} + def to_dict(self): + """ + Returns the model properties as a dict + """ + result = {} + + for attr, _ in iteritems(self.swagger_types): + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + + return result + + def to_str(self): + """ + Returns the string representation of the model + """ + return pformat(self.to_dict()) + + def __repr__(self): + """ + For `print` and `pprint` + """ + return self.to_str() + + def __eq__(self, other): + """ + Returns true if both objects are equal + """ + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """ + Returns true if both objects are not equal + """ + return not self == other + +{{/model}} +{{/models}} diff --git a/tests/test_auth_groups.py b/tests/test_auth_groups.py index cb23126..5a4b602 100644 --- a/tests/test_auth_groups.py +++ b/tests/test_auth_groups.py @@ -47,14 +47,15 @@ auth_group=updateAuthGroup) # try adding a member -groupMember = isi_sdk.GroupsGroupMember() +groupMember = isi_sdk.GroupMember() groupMember.name = "admin" groupMember.type = "user" -authApi.create_groups_group_member(group=newAuthGroup.name, - groups_group_member=groupMember) +authGroupsApi = isi_sdk.AuthGroupsApi(apiClient) +authGroupsApi.create_group_member(group=newAuthGroup.name, + group_member=groupMember) -groupMembers = authApi.list_groups_group_members(group=newAuthGroup.name) +groupMembers = authGroupsApi.list_group_members(group=newAuthGroup.name) foundMember = False for member in groupMembers.members: if member.name == groupMember.name: @@ -63,10 +64,10 @@ print "Found member: " + str(foundMember) # delete the member -authApi.delete_groups_group_member(group=newAuthGroup.name, - groups_group_member_id=groupMembers.members[0].id) +authGroupsApi.delete_group_member(group=newAuthGroup.name, + group_member_id=groupMembers.members[0].id) -groupMembers = authApi.list_groups_group_members(group=newAuthGroup.name) +groupMembers = authGroupsApi.list_group_members(group=newAuthGroup.name) foundMember = False for member in groupMembers.members: if member.name == groupMember.name: diff --git a/tests/test_cluster_config.py b/tests/test_cluster_config.py new file mode 100644 index 0000000..f4266ca --- /dev/null +++ b/tests/test_cluster_config.py @@ -0,0 +1,22 @@ +import isi_sdk +import urllib3 + +urllib3.disable_warnings() + +# configure username and password +isi_sdk.configuration.username = "root" +isi_sdk.configuration.password = "a" +isi_sdk.configuration.verify_ssl = False + +# configure host +host = "https://VNODE2294.west.isilon.com:8080" +apiClient = isi_sdk.ApiClient(host) +clusterApi = isi_sdk.ClusterApi(apiClient) + +# these two end points were throwing exceptions before so just testing that +# they have any response at all for now. +print str(clusterApi.get_cluster_config()) +print "It worked." + +print str(clusterApi.get_cluster_version()) +print "Done." diff --git a/tests/test_event_eventgroup_ocurrences.py b/tests/test_event_eventgroup_ocurrences.py new file mode 100644 index 0000000..b5131aa --- /dev/null +++ b/tests/test_event_eventgroup_ocurrences.py @@ -0,0 +1,25 @@ +import isi_sdk +import urllib3 + +urllib3.disable_warnings() + +# configure username and password +isi_sdk.configuration.username = "root" +isi_sdk.configuration.password = "a" +isi_sdk.configuration.verify_ssl = False + +# configure host +host = "https://VNODE2294.west.isilon.com:8080" +apiClient = isi_sdk.ApiClient(host) +eventApi = isi_sdk.EventApi(apiClient) + +resp = eventApi.get_event_eventgroup_occurrences() +print "It worked." +# This is broken in 8.0 +#if resp.total > 1 \ +# and type(resp.eventgroup_occurrences) == list \ +# and len(resp.eventgroup_occurrences) > 0: +# print "It worked." +#else: +# print str(resp) +# print "Failed." diff --git a/tests/test_quotas.py b/tests/test_quotas.py new file mode 100644 index 0000000..5a659a9 --- /dev/null +++ b/tests/test_quotas.py @@ -0,0 +1,31 @@ +import isi_sdk +import urllib3 + +urllib3.disable_warnings() + +# configure username and password +isi_sdk.configuration.username = "root" +isi_sdk.configuration.password = "a" +isi_sdk.configuration.verify_ssl = False + +# configure host +host = "https://VNODE2294.west.isilon.com:8080" +apiClient = isi_sdk.ApiClient(host) +quotaApi = isi_sdk.QuotaApi(apiClient) + +newQuota = isi_sdk.QuotaQuotaCreateParams() +newQuota.enforced = False +newQuota.include_snapshots = False +newQuota.thresholds_include_overhead = False +newQuota.path = "/ifs/data" +newQuota.type = "directory" + +createResp = quotaApi.create_quota_quota(quota_quota=newQuota) +print "Created=" + str(createResp) + +print str(quotaApi.list_quota_quotas()) + +deleteResp = quotaApi.delete_quota_quotas(path=newQuota.path) +print str(deleteResp) + +print "It worked."