Allows cron job control and command execution on a remote machine using git infrastructure


License
MIT-feh
Install
pip install git-syncer==1.1.1

Documentation

Git syncer

Allows cron job control and command execution on a remote machine using git infrastructure.

Tests Lint PyPI version

👷🏻 Please note, this project is still a work-in-progress 🏗️

Quick start

On your computer, set up the basic repository (checkout the example_project in this repository to see how this should generally look):

  1. Create a private GitHub repository and clone it.
  2. Create a requirements.txt file, and add the git-syncer dependency.
  3. Create a main.py file:
from git_syncer import run
from git_syncer.runnables import register

register()  # Add boot jobs, cron jobs, and remote commands here 🏋🏻‍♂️

if __name__ == "__main__":
    run()
  1. Add a .gitignore file (tip: use gitignore.io). It must contain out/ directory.
  2. Commit and push your changes.

On your remote machine (SSH to it):

  1. Set up GitHub credentials to clone your new repository.
    Highly recommended: instead of providing your own personal GitHub credentials on the remote machine, add an SSH deploy key to your repository . If you choose to do so, check the Allow write access checkbox.
  2. Clone your new repository to the remote machine.
  3. Set up a virtual environment for this project and activate it (using source <venv_dir>/bin/activate).
  4. cd to your repository directory.
  5. Install dependencies using pip install -r requirements.txt (git-syncer should be installed).
  6. ⚠️ THIS STEP WILL OVERRIDE YOUR EXISTING CRONTAB SETTINGS! ⚠️
    Activate the syncer using the CLI command init-syncer.

From now on, you can add new cron jobs and execute remote commands on the remote machine using this git repository. For more details, see the usage section.

Usage

Note: this tool writes logs to ~/logs/git-syncer/.

Runnables

A Runnable is the basic class that the package uses. To define your own custom commands, create your own class, inherit Runnable, and implement the mandatory abstract methods:

# File: my_runnables.py
from git_syncer.models import Runnable


class HelloWorld(Runnable):
    @property
    def verbose_name(self) -> str:
        return "Hello World"

    def run(self) -> str:
        return "This runnable was called!"

...and register your runnable in main.py:

# File: main.py
from git_syncer.runnables import register

from my_runnables import HelloWorld

register(HelloWorld())

Boot jobs

Boot jobs will execute once the remote machine turns on. In order to make a Runnable into a boot job, set the run_on_boot property to True:

class HelloWorld(Runnable):
    @property
    def run_on_boot(self) -> bool:
        return True
    ...

Cron jobs

To create a cron job, inherit the CronJob class, and fill the expression property:

from git_syncer.models import CronJob

class MyCronJob(CronJob):
    @property
    def verbose_name(self) -> str:
        return "Ping"

    @property
    def expression(self) -> str:
        # Every 5 minutes
        return "*/5 * * * *"

    def run(self) -> str:
        return "This job runs every 5 minutes"

Execute jobs on command

In order to make a non-cron runnable execute on the remote machine:

  1. On your local machine, commit an empty file matching your Runnable name under execute folder (for example, if the runnable class name is GetIP, commit a file named execute/get-ip).
  2. Push your changes.
  3. On the next round minute:
    1. The runnable will execute on the remote device,
    2. The execution result will be written in a file matching your runnable name (for example, if the runnable class name is GetIP, the result file will be named execute/get-ip-result.txt, and the execute/get-ip file will be removed).
    3. The changes will be committed and pushed back to the repository.
  4. Wait a few seconds and pull your repository. You will see the execution result in the expected result file.

Expert mode: every minute, in order the check if a non-cron Runnable should be executed, its should_execute method is called (matching file names to the runnable class name). In order to execute your runnable based on different logic, override the should_execute method:

from typing import Set
from git_syncer.models import Runnable
import random

class HelloWorld(Runnable):
    @property
    def verbose_name(self) -> str:
        return "Hello World"

    def run(self) -> str:
        return "This runnable was called!"

     def should_execute(self, inputs: Set[str]) -> bool:
        # Take a look at the base method and implement your own logic.
        return random.randint(1, 100) % 5 == 0

Error handling

👷🏻 TODO