Azure / azure-sdk-for-python

This repository is for active development of the Azure SDK for Python. For consumers of the SDK we recommend visiting our public developer docs at https://learn.microsoft.com/python/azure/ or our versioned developer docs at https://azure.github.io/azure-sdk-for-python.
MIT License
4.53k stars 2.76k forks source link

azure.mgmt.scheduler.operations.JobsOperations.create_update() Malformed Job Object #1814

Closed cancanf3 closed 5 years ago

cancanf3 commented 6 years ago

Hello,

I am trying to create a new Job in the Azure Scheduler using the Python SDK. However, the respond from the API is not providing enough details of the error and I have tried to find my own problem re-checking the API's doc multiple times but I haven't been able to found the error.

Here is the stack trace of the respond from the API

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/student/DataLions/Server/DataLionsAPI/rest/AzureScheduler.py", line 56, in create
    self._setJobDefinition(event))
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/azure/mgmt/scheduler/operations/jobs_operations.py", line 164, in create_or_update
    raise exp
msrestazure.azure_exceptions.CloudError: Azure Error: BadRequest
Message: Malformed Job Object

I have replaced subscriptionId and the uri with a mock one for security purposes.

Here is the code I am using to send the request.

Important! event['endDate'] = "2018-01-19"

from msrest.serialization import Serializer, Deserializer
from msrest.configuration import Configuration
from msrest.service_client import ServiceClient
from azure.mgmt.scheduler import SchedulerManagementClient
from azure.mgmt.scheduler.operations import JobsOperations
from azure.mgmt.scheduler.models import JobDefinition, JobProperties, JobAction
from azure.mgmt.scheduler.models import HttpRequest, HttpAuthentication, JobState
from azure.mgmt.scheduler.models import JobRecurrence, RetryPolicy, StorageQueueMessage
from azure.mgmt.scheduler.models import JobActionType
from datetime import datetime, timedelta
import importlib, inspect

# Wrapper for the Azure Microsoft API
class AzureScheduler:

    # The constructor setups the essential configuration to use the Scheduler
    # from Microsoft Azure
    def __init__(self, credentials, subscriptionId="mocksubscriptionId",
        resourceGroupName="syncResource", jobCollectionName="apisync"):

        self.subscriptionId = subscriptionId
        self.resourceGroupName = resourceGroupName
        self.jobCollectionName = jobCollectionName
        self.schedulerClient = SchedulerManagementClient(credentials, subscriptionId)
        self.serviceClient = ServiceClient(credentials,Configuration("https://management.azure.com/"))

        classes = self.__setClasses()

        self.jobsOperations = JobsOperations(self.serviceClient, self.schedulerClient.config,
        Serializer(classes=classes), Deserializer(classes=classes))

    # Setup a Dictionary with all the classes from the Scheduler Model.
    # Required to Serialize and Deserialize objects/JSONs
    def __setClasses(self):
        classes = {}
        model = "azure.mgmt.scheduler.models"
        mod =  importlib.import_module(model)
        for mod_name, mod_obj in inspect.getmembers(mod, predicate=inspect.isclass):
             classes[mod_name] = mod_obj
        return classes

    # List all the Jobs inside the Job Collection (jobCollectionName)
    def list(self):
        return self.jobsOperations.list(self.resourceGroupName,self.jobCollectionName)

    # first jobName datalionScheduler
    # Gets the description of the job requested
    def get(self, jobName):
        return self.jobsOperations.get(self.resourceGroupName,self.jobCollectionName,jobName)

    # Creates a job in the Job Collection
    def create(self, jobName, event):
        return self.jobsOperations.create_or_update(self.resourceGroupName,
                        self.jobCollectionName, jobName,
                        self._setJobDefinition(event))

    # Deletes the job (jobName)
    def delete(self, jobName):
        return self.jobsOperations.delete(self.resourceGroupName,self.jobCollectionName,jobName)

    def _setJobDefinition(self, event):
        return JobDefinition(properties=self._setJobProperties(event))

    def _setJobProperties(self, event):
        return JobProperties(
            start_time=datetime.strptime(event['endDate']+ " 9:50pm -0500", '%Y-%m-%d %I:%M%p %z'),
            action=self._setJobAction(),
            state=JobState("Enabled"),
            recurrence=self._setJobRecurrence(event))

    def  _setJobAction(self):
        return JobAction(type=JobActionType('Http'),request=self._setHttpRequest(), retry_policy=self._setRetryPolicy(), queue_message=self._setStorageQueueMessage())

    def _setHttpRequest(self):
        return HttpRequest(uri="http://test.ngrok.io/rest/player/?format=json", method='GET', authentication=HttpAuthentication('NotSpecified'), body="", headers={})

    def _setStorageQueueMessage(self):
        return StorageQueueMessage(storage_account="datalions", queue_name="Training Session", sas_token=None, message="Sync after Training")

    def _setRetryPolicy(self):
        return RetryPolicy(retry_type='Fixed', retry_interval=timedelta(minutes=3), retry_count=3)

    def _setJobRecurrence(self, event):
        return JobRecurrence(
            frequency='Hour',
            interval=1,end_time=datetime.strptime(event['endDate']+ " 11:50pm -0500",
            '%Y-%m-%d %I:%M%p %z'))
lmazuel commented 6 years ago

Hi @cancanf3 Could you enable DEBUG logging and post the JSON you actually send to Azure? Thanks!

cancanf3 commented 6 years ago

Thanks for your response @lmazuel DEBUG is already enabled in Django settings. I am using the Python SDK for Azure to build the job object, I am not building a JSON.

Since the request is sent through HTTPS, I can not sniff it. Could you suggest another way to provide you more details?

lmazuel commented 6 years ago

I don't how your Django settings are set, but you should be able to get logging from the "msrest" logger. And yes, you are building a JSON under the hood to communicate with Azure :)

If you can't get the logs from "msrest" logger, you should be able to print the serialization of your model. You can print an instance of JobDefinition using:

job_definition = self._setJobDefinition(event)
print(job_definition.serialize())  # will print the JSON sent to Azure
cancanf3 commented 6 years ago

Haha yes! but Azure is doing it, not me. :P

I tried to inject the adapter and request to the logger but I haven't figure it out yet (sorry, I am new on this)

However here is the JSON of the request. I hope this help you, if not I will try again to setup the logger.

{
   'properties': {
      'startTime': '2017-02-21T02:50:00.000Z', 
      'recurrence': {
         'endTime': '2017-02-21T04:50:00.000Z', 
         'interval': 1, 
         'frequency': 'Hour'
      }, 
      'action': {
         'type': 'Http', 
         'retryPolicy': {
            'retryInterval': 'PT3M', 
            'retryCount': 3, 
            'retryType': 'Fixed'
         }, 
         'request': {
            'body': '', 'uri': 
            'http://15b7b5a1.ngrok.io/rest/player/?format=json', 
            'authentication': {
               'type': 'NotSpecified'
            }, 
         'headers': {}, 
         'method': 'GET'
         }, 
         'queueMessage': {
            'queueName': 'Training Session',
            'message': 'Sync after Training', 
            'storageAccount': 'datalions'
         }
      }, 
   'state': 'Enabled'
   }
}
lmazuel commented 6 years ago

Could you create the job manually through the portal, and then go on: https://resources.azure.com

On this website, you should be able to find your job resource. On the left panel, open "subscriptions", then your subscription name, then "providers", then "Microsoft.Scheduler" and find your manually created job. Then post the JSON you'll see here, so we can compare what the Portal has created and see what Python is messing up.

cancanf3 commented 6 years ago

Sorry for the Delay. This is the JSON of a job that I created from the Azure webapp

"properties": {
    "startTime": "2018-01-13T17:52:30.463Z",
    "action": {
      "request": {
        "uri": "http://15b7b5a1.ngrok.io/rest/player/?format=json",
        "method": "GET"
      },
      "type": "HTTP",
      "retryPolicy": {
        "retryType": "fixed"
      }
    },
    "recurrence": {
      "frequency": "day",
      "endTime": "2018-02-12T16:52:30.463Z",
      "interval": 1
    },
    "state": "enabled",
    "status": {
      "lastExecutionTime": "2018-01-28T17:54:30.8499069Z",
      "nextExecutionTime": "2018-01-29T17:52:30.463Z",
      "executionCount": 35,
      "failureCount": 25,
      "faultedCount": 5
    }
  }
lmazuel commented 6 years ago

Could you confirm the version of your package (pip freeze)? And really try to create the exact same job through the portal (for instance the last one you mentionned doesn't have the same retryPolicy).

cancanf3 commented 6 years ago

I had to change the retryPolicy in the one I am trying to create from django because the tier doesn't allow minutes.

package version: azure-mgmt-scheduler==1.1.3 msrest==0.4.25 msrestazure==0.4.20

Therefore I am sending you the new json from django that still has the same exception and the one from azure.

From Python SDK:

{
   'properties': {
      'state': 'Enabled', 
      'action': {
         'retryPolicy': {
            'retryType': 'Fixed', 
            'retryCount': 3, 
            'retryInterval': 'PT3H'
         }, 
      'queueMessage': {
         'storageAccount': 'datalions', 
         'queueName': 'Training Session', 
         'message': 'Sync after Training'
      }, 
      'type': 'Http', 
      'request': {
         'method': 'GET', 
         'uri': 'http://15b7b5a1.ngrok.io/rest/player/?format=json', 
         'body': '', 'authentication': {
            'type': 'NotSpecified'
         }, 
         'headers': {}
      }
   }, 
   'recurrence': {
      'frequency': 'Day', 
      'endTime': '2018-02-13T04:50:00.000Z', 
      'interval': 1
   }, 
   'state': 'Enabled', 
   'startTime': '2018-02-13T02:50:00.000Z'
}

From https://resources.azure.com

{
  "id": "/subscriptions/3b82224f-b5cc-4355-aefb-fe16f46b05a4/resourceGroups/syncResource/providers/Microsoft.Scheduler/jobCollections/apisync/jobs/datalionScheduler",
  "type": "Microsoft.Scheduler/jobCollections/jobs",
  "name": "apisync/datalionScheduler",
  "properties": {
    "startTime": "2018-01-13T17:52:30.463Z",
    "action": {
      "request": {
        "uri": "http://15b7b5a1.ngrok.io/rest/player/?format=json",
        "method": "GET"
      },
      "type": "HTTP",
      "retryPolicy": {
        "retryType": "fixed",
        "retryInterval": "03:00:00",
        "retryCount": 3
      }
    },
    "recurrence": {
      "frequency": "day",
      "endTime": "2018-02-12T16:52:30.463Z",
      "interval": 1
    },
    "state": "enabled",
    "status": {
      "nextExecutionTime": "2018-01-31T17:52:30.463Z",
      "executionCount": 0,
      "failureCount": 0,
      "faultedCount": 0
    }
  }
}
lmazuel commented 6 years ago

The only thing I can see right now, is the retryInterval, but it's supposed to be fixed in 1.1.3 (it's why I asked you the version). Could you try to replace timedelta(minutes=3) by the string "03:00:00"? If it works, I know what I have to fix (again....). If it doesn't, at this point I need to take some time to try to repro myself :/ Thanks for your patience,

cancanf3 commented 6 years ago

It's all good! I am always enjoy contributing.

Alright, so I tried your suggestion but I got an exception:

Internal Server Error: /rest/calendar/
Traceback (most recent call last):
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 592, in serialize_data
    return self.serialize_type[data_type](data, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 801, in serialize_duration
    attr = isodate.parse_duration(attr)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/isodate/isoduration.py", line 104, in parse_duration
    raise ISO8601Error("Unable to parse duration string %r" % datestring)
isodate.isoerror.ISO8601Error: Unable to parse duration string '03:00:00'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/django/core/handlers/exception.py", line 35, in inner
    response = get_response(request)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/django/core/handlers/base.py", line 128, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/django/core/handlers/base.py", line 126, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/django/views/generic/base.py", line 69, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/rest/views/CalendarView.py", line 32, in post
    AzureScheduler(credentials).create(str(request.data['game'])+request.data['startDate'], request.data)
  File "/home/student/DataLions/Server/DataLionsAPI/rest/AzureScheduler.py", line 58, in create
    print(self._setJobDefinition(event).serialize())
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 196, in serialize
    return serializer._serialize(self, keep_readonly=keep_readonly)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 421, in _serialize
    orig_attr, attr_type, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 610, in serialize_data
    return self._serialize(data, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 421, in _serialize
    orig_attr, attr_type, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 610, in serialize_data
    return self._serialize(data, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 421, in _serialize
    orig_attr, attr_type, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 610, in serialize_data
    return self._serialize(data, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 421, in _serialize
    orig_attr, attr_type, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 608, in serialize_data
    SerializationError, msg.format(data, data_type), err)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/exceptions.py", line 45, in raise_with_traceback
    raise error.with_traceback(exc_traceback)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 592, in serialize_data
    return self.serialize_type[data_type](data, **kwargs)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/msrest/serialization.py", line 801, in serialize_duration
    attr = isodate.parse_duration(attr)
  File "/home/student/DataLions/Server/DataLionsAPI/env/lib/python3.5/site-packages/isodate/isoduration.py", line 104, in parse_duration
    raise ISO8601Error("Unable to parse duration string %r" % datestring)
msrest.exceptions.SerializationError: Unable to serialize value: '03:00:00' as type: 'duration'., ISO8601Error: Unable to parse duration string '03:00:00'

Therefore I also tried setting retry_interval=None and retry_count=None. But I still get the same error.

lmazuel commented 5 years ago

I won't fix this issue, since Scheduler is now officially deprecated. This is the disclaimer from the team I just received:

Move from Azure Scheduler to Azure Logic Apps by September 30, 2019

You’re receiving this email because you currently use Azure Scheduler. In 2016, we launched Azure Logic Apps, which includes the ability to configure and run scheduled jobs as well as advanced workflow features. Because Logic Apps fully replaces Azure Scheduler capabilities, Scheduler will be retired as a standalone service on September 30, 2019. As with all changes of this type, we’re providing 12 months’ notice so you have adequate time to adjust. To continue using jobs you’ve configured in Scheduler, please move to Logic Apps by September 30, 2019. We encourage you to make the switch sooner, to gain the richer benefits of Logic Apps. In addition to the Scheduler features you’re already familiar with, Logic Apps enables you to:

  • Use a visual designer and connectors to integrate with more than 200 different services, including Azure Blob storage, Azure Service Bus, Microsoft Outlook, and SAP.
  • Manage each scheduled workload as a first-class Azure resource.
  • Run multiple one-time jobs using a single logic app.
  • Set schedules that automatically adjust to daylight saving time.

Required action Move your scheduled jobs to Logic Apps by September 30, 2019, to take advantage of advanced capabilities and avoid any service disruptions. Read the migration guidance and FAQ to begin transitioning your current jobs from Azure Scheduler to Azure Logic Apps. If you have questions, please contact us.