socialwifi / RouterOS-api

Python API to RouterBoard devices produced by MikroTik.
MIT License
251 stars 100 forks source link

How can you run a Mikrotik script, and get the output? #71

Closed tal-zvon closed 2 months ago

tal-zvon commented 3 years ago

Even after reading the Readme, I can't figure out how to run a Mikrotik script, and get its output.

After some digging, running my script seems to work with this:

#!/usr/bin/env python3

import routeros_api

args = {
    "host": "1.2.3.4",
    "username": "admin",
    "password": "PASSWORD",
        ...
}

api_connection = routeros_api.RouterOsApiPool(**args)
api = api_connection.get_api()

scripts = api.get_resource("/system/script")

script_to_run = scripts.get(name="my_script")[0]

async_response = api.get_binary_resource('/').call('system/script/run', {"number": script_to_run["id"].encode("utf-8")})

Super complicated, but ok. Seems to work.

Unfortunately, the async_response, which is of type routeros_api.api_communicator.base.AsynchronousResponse, doesn't seem to have any way to retrieve the script output.

My script just has a single /put "hello" line.

If you print async_response, it just comes back as []. If you try to convert it to a list, it's still an empty list.

The fact that the class is called AsynchronousResponse implies that it may be a non-blocking operation that returns before the output is ready. dir(async_response) shows no methods to call that will wait for the output to be ready, and adding /delay 15 to my script makes the call take 15 seconds longer, implying this is a synchronous call.

Is there no way to get script output? Am I calling scripts wrong?

InfosecBlake commented 3 years ago

This is what I did:

scripts = api.get_resource("/system/script") script_to_run = scripts.get(name='your_script')[0] pprint(script_to_run['source'])

This will print out the contents "source" of whatever script that you name. I use this to verify the script before it is ran.

You should also be able to do:

pprint(script_to_run['last-started'])

This will print the last time that the script was ran.

tal-zvon commented 3 years ago

@InfosecBlake Where's the part that actually runs the script? Seeing the source doesn't help if I can't run it

InfosecBlake commented 3 years ago

I believe that you had it correct. I my code I did the following:

api.get_binary_resource('/').call('system/script/run',{"number": script_to_run["name"].encode("utf-8")})

I deciced to use "name" instead of "id" due to Mikrotik changing the value of "id" every time a script is ran. This worked for me.

I hope this helps!

tal-zvon commented 3 years ago

Sorry it's been a while since I've tried this. Yes, my original post does work - that wasn't the problem. The problem is that there is no obvious way to get the script output after it runs.

Also, the way I ran the script was ridiculously complicated. Is this really the easiest (or only) way to run a script using the API? Why is this not just output = api.run_script('script_name')? Is someone allergic to simplicity? The way I did it doesn't even appear to be documented anywhere. I don't recall how I even figured it out.

Brasswyrm commented 2 years ago

Did you ever figure this out? I am trying to do the same thing...

InfosecBlake commented 2 years ago

Brasswyrm,

The following will run the script: api.get_binary_resource('/').call('system/script/run',{"number": script_to_run["name"].encode("utf-8")})

I've honestly gotten away from this API client because it doesn't seem to be maintained. I made a tool for configuring Mikrotik that uses SSH and SFTP. I found that building my own tool was easier than using the API.

Brasswyrm commented 2 years ago

@InfosecBlake

Thanks, I tried to do what I was doing via Paramiko and it was so much easier. After banging my head against it for a couple of days using SSH fixed my issue almost instantly!

InfosecBlake commented 2 years ago

@Brasswyrm Paramiko is great, and works like a charm. If you want to run multiple commands in one session, see my project. My config tool pushes all the commands at once, one line at a time.

tal-zvon commented 2 years ago

Paramiko is great, but the API, or at least a properly-maintained, easy-to-use API, clearly has advantages. For example, if you want to list the IP addresses on the mikrotik, with the API, you get a list of dicts (super easy to work with), while SSH/Paramiko will give you a string back that you need to parse. Parsing strings is error-prone. The output generated when running command through SSH is not designed to be parsed by computers - it's designed to be looked at by people, so the developers can change it anytime they feel like, or show some extra warning line in some uncommon situations which will break scripts.

tal-zvon commented 2 years ago

Accidentally closed the issue while writing a comment on my phone. I never found a way to read the output of a script when running it through this API, or a simpler way to run it using the API.

Brasswyrm commented 2 years ago

@tal-zvon

Thanks for the insight, I'll definitely keep that in mind. For my particular situation, I am not really using the routers in any sort of "networking" capacity. I am basically just using them as a 60GHz source for some personal research. So for me, even if something breaks down the line it should be fine because the script that I am writing does not need to be super robust or anything. But if I were to use these routers for an application where the script needs to be more bulletproof, I totally agree that the API would be better.

tal-zvon commented 2 years ago

@Brasswyrm in your situation, I would definitely suggest using Paramiko, especially since there's no known way to get output from a script using the API. I use Paramiko extensively with mikrotiks in such situations as well.

jgoclawski commented 2 months ago

For anyone reading this, here's a working example (tested on RouterOS v. 7.3.1):

>>> api.get_resource("/system/script").get()[0]['source']
'/put "hello"'
>>> async_response = api.get_binary_resource('/').call('system/script/run', {"number": '0'.encode('utf-8')})
>>> async_response.__dict__
{'command': <routeros_api.sentence.CommandSentence object at 0x73a0f2b3eba0>, 'done_message': {'ret': b'hello'}, 'done': True, 'error': None}
>>> async_response.done_message['ret']
b'hello'