Skip to content

Commit

Permalink
Merge pull request #20 from alexzhangs/develop
Browse files Browse the repository at this point in the history
Develop

Update install.sh: add SSM_VERSION.
Update ci-unittest.yml: do not fail CI on codecov uploader fails and update docker image to shadowsocks-libev-v2ray.
Update ci-docker.yml: use the explicit version of docker image.
Update ci-testpypi.yml and ci-pypi.yml: narrow down the environment variables scope.
Update shadowsocks/admin.py remove is_v2ray_enabled from readonly_fields.

Update shadowsocks/models.py:
* update SSManager.interface default value from LOCALHOST to PRIVATE.
* set default sender for Account.notify().

Update notification/fixtures:
* improve template content and format.
* add template.txt and template-txt-to-json.py.

Update README.md: update screenshots.
  • Loading branch information
alexzhangs committed May 9, 2024
2 parents 9f3e679 + 70e3960 commit a5d78c0
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 41 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jobs:
docker-build-and-push:
runs-on: ubuntu-latest
outputs:
VERSION: ${{ steps.get_version.outputs.VERSION }}
VERSION: ${{ steps.get-version.outputs.VERSION }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get version
id: get_version
id: get-version
run: |
version=$(awk -F\" '/^version = / {print $2}' pyproject.toml)
echo "VERSION=$version" >> $GITHUB_ENV
Expand Down Expand Up @@ -58,5 +58,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Docker container
env:
SSM_VERSION: ${{ needs.docker-build-and-push.outputs.VERSION }}
run: |
bash install.sh
11 changes: 5 additions & 6 deletions .github/workflows/ci-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ on:
- 'ci-pypi'
- 'ci-pypi-(major|minor|patch|suffix)'

env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

jobs:
pypi-py3-build-and-upload:
runs-on: ubuntu-latest
Expand All @@ -39,7 +35,6 @@ jobs:
NEW_VERSION: ${{ steps.bump-version.outputs.NEW_VERSION }}
steps:
- name: Get version part
id: get_version_part
run: |
case ${{ github.event_name }} in
workflow_dispatch)
Expand Down Expand Up @@ -70,13 +65,17 @@ jobs:
run: |
bump-my-version bump --verbose "${{ env.VERSION_PART }}"
current_version=$(bump-my-version show-bump | awk 'NR==1 {print $1}')
echo "NEW_VERSION=${current_version}" >> $GITHUB_ENV
echo "NEW_VERSION=${current_version}" >> $GITHUB_OUTPUT
- name: Build source and binary distributions
run: python -m build
- name: Upload to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: twine upload dist/*
- name: Push commit and tag
run: git push origin HEAD:master refs/tags/${{ steps.bump-version.outputs.NEW_VERSION }}
run: git push origin HEAD:master refs/tags/${{ env.NEW_VERSION }}

pypi-py3-install:
runs-on: ubuntu-latest
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci-testpypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ on:
- cron: '23 11 1 * *'

env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TESTPYPI_TOKEN }}
PROJECT_NAME_FOR_TESTPYPI: shadowsocks-manager-alexzhangs

jobs:
Expand Down Expand Up @@ -82,6 +80,9 @@ jobs:
- name: Build source and binary distributions
run: python -m build
- name: Upload to TestPyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TESTPYPI_TOKEN }}
run: twine upload --repository testpypi dist/*

testpypi-py3-install:
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/ci-unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,30 +64,30 @@ jobs:
docker run -d -p 11211:11211 --name ssm-dev-memcached memcached
docker run -d -p 5672:5672 --name ssm-dev-rabbitmq rabbitmq
# run shadowsocks-libev, simulate localhost node
# run shadowsocks-libev-v2ray, simulate localhost node
MGR_PORT=6001 SS_PORTS=8381-8479 ENCRYPT=aes-256-cfb
docker run -d -p 127.0.0.1:$MGR_PORT:$MGR_PORT/UDP \
-p 127.0.0.1:$SS_PORTS:$SS_PORTS/UDP -p 127.0.0.1:$SS_PORTS:$SS_PORTS \
--name ssm-dev-ss-libev-localhost shadowsocks/shadowsocks-libev:edge \
--name ssm-dev-ss-libev-localhost alexzhangs/shadowsocks-libev-v2ray \
ss-manager --manager-address 0.0.0.0:$MGR_PORT \
--executable /usr/local/bin/ss-server -m $ENCRYPT -s 0.0.0.0 -u
# get private IP
PRIVATE_IP=$(hostname -i | awk '{print $1}')
# run shadowsocks-libev, simulate private IP node
# run shadowsocks-libev-v2ray, simulate private IP node
MGR_PORT=6002 SS_PORTS=8381-8479 ENCRYPT=aes-256-cfb
docker run -d -p $PRIVATE_IP:$MGR_PORT:$MGR_PORT/UDP \
-p $PRIVATE_IP:$SS_PORTS:$SS_PORTS/UDP -p $PRIVATE_IP:$SS_PORTS:$SS_PORTS \
--name ssm-dev-ss-libev-private shadowsocks/shadowsocks-libev:edge \
--name ssm-dev-ss-libev-private alexzhangs/shadowsocks-libev-v2ray \
ss-manager --manager-address 0.0.0.0:$MGR_PORT \
--executable /usr/local/bin/ss-server -m $ENCRYPT -s 0.0.0.0 -u
# run shadowsocks-libev, simulate public IP node
# run shadowsocks-libev-v2ray, simulate public IP node
MGR_PORT=6003 SS_PORTS=8480 ENCRYPT=aes-256-cfb
docker run -d -p $PRIVATE_IP:$MGR_PORT:$MGR_PORT/UDP \
-p $PRIVATE_IP:$SS_PORTS:$SS_PORTS/UDP -p $PRIVATE_IP:$SS_PORTS:$SS_PORTS \
--name ssm-dev-ss-libev-public shadowsocks/shadowsocks-libev:edge \
--name ssm-dev-ss-libev-public alexzhangs/shadowsocks-libev-v2ray \
ss-manager --manager-address 0.0.0.0:$MGR_PORT \
--executable /usr/local/bin/ss-server -m $ENCRYPT -s 0.0.0.0 -u
- name: Install this project
Expand All @@ -97,6 +97,7 @@ jobs:
- name: Run Django testcases
run: ssm-test -c
- name: Upload coverage report to Codecov
continue-on-error: true # codecov uploader occasionally fails, e.g.: upstream request timeout
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: ssm-test -u
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,18 @@ Code in Python, base on Django, Django REST framework, Celery, and SQLite.

The development status can be found at: [project home](https://github.com/alexzhangs/shadowsocks-manager/projects/1).


## Screenshots

Shadowsocks Node List:
![Home › Shadowsocks › Shadowsocks Nodes](https://www.0xbeta.com/shadowsocks-manager/assets/images/shadowsocks-node-list.png)

Add Shadowsocks Node:
![Home › Shadowsocks › Shadowsocks Nodes](https://www.0xbeta.com/shadowsocks-manager/assets/images/shadowsocks-node-add.png)

Add Shadowsocks Account:
![Home › Shadowsocks › Shadowsocks Accounts](https://www.0xbeta.com/shadowsocks-manager/assets/images/shadowsocks-account-add.png)


## 1. Requirements

Expand Down
Binary file added assets/images/shadowsocks-account-add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/shadowsocks-node-add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 46 additions & 21 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,22 @@
#? -e SSM_MEMCACHED_HOST=ssm-memcached
#? -e SSM_RABBITMQ_HOST=ssm-rabbitmq
#?
#? Environment:
#? The following environment variables are used by this script:
#?
#? - SSM_VERSION
#?
#? Optional.
#? Set the version of the shadowsocks-manager Docker image.
#? The default value is `latest`.
#?
#? Example:
#? # quick start with default options
#? $ bash install.sh
#?
#? # install a specific version of shadowsocks-manager
#? $ SSM_VERSION=0.1.5 bash install.sh
#?

# exit on any error
set -e -o pipefail
Expand Down Expand Up @@ -52,51 +64,64 @@ function guid () {
}

function main () {

if [[ $1 == '-h' || $1 == '--help' ]]; then
usage
exit 0
fi

check-os
check-docker

declare -a default_options=(
-e "SSM_SECRET_KEY=$(guid)"
-e "SSM_DEBUG=False"
-e "SSM_MEMCACHED_HOST=ssm-memcached"
-e "SSM_RABBITMQ_HOST=ssm-rabbitmq")

check-os
check-docker
declare SSM_VERSION=${SSM_VERSION:-latest}
declare ssm_image="alexzhangs/shadowsocks-manager:$SSM_VERSION"
declare ssm_container_name="ssm-$SSM_VERSION"
declare ssm_network_name="${ssm_container_name}-network"
declare memcached_container_name="${ssm_container_name}-memcached"
declare rabbitmq_container_name="${ssm_container_name}-rabbitmq"

declare volume_path=~/ssm-volume
declare ssm_volume_path=~/"${ssm_container_name}-volume"

# create volume path on host
mkdir -p "$volume_path"
mkdir -p "$ssm_volume_path"

echo "Removing the existing container and network if any ..."
if docker ps -a --format '{{.Names}}' | grep -q '^ssm-memcached$'; then
docker rm -f ssm-memcached
if docker ps -a --format '{{.Names}}' | grep -q "^${memcached_container_name}$"; then
docker rm -f "$memcached_container_name"
fi

if docker ps -a --format '{{.Names}}' | grep -q '^ssm-rabbitmq$'; then
docker rm -f ssm-rabbitmq
if docker ps -a --format '{{.Names}}' | grep -q "^${rabbitmq_container_name}$"; then
docker rm -f "$rabbitmq_container_name"
fi

if docker ps -a --format '{{.Names}}' | grep -q '^ssm$'; then
docker rm -f ssm
if docker ps -a --format '{{.Names}}' | grep -q "^${ssm_container_name}$"; then
docker rm -f "$ssm_container_name"
fi

if docker network inspect ssm-network &>/dev/null; then
docker network rm ssm-network
if docker network inspect "$ssm_network_name" &>/dev/null; then
docker network rm "$ssm_network_name"
fi

echo "Creating ssm-network ..."
docker network create ssm-network
echo "Creating $ssm_network_name ..."
docker network create "$ssm_network_name"

echo "Running ssm-memcached ..."
docker run --restart=always -d --network ssm-network --name ssm-memcached memcached
echo "Running $memcached_container_name ..."
docker run --restart=always -d --network "$ssm_network_name" --name "$memcached_container_name" memcached

echo "Running ssm-rabbitmq ..."
echo "Running $rabbitmq_container_name ..."
# run rabbitmq, used by celery
docker run --restart=always -d --network ssm-network --name ssm-rabbitmq rabbitmq
docker run --restart=always -d --network "$ssm_network_name" --name "$rabbitmq_container_name" rabbitmq

# run shadowsocks-manager
echo "Running ssm ..."
docker run --restart=always -d -p 80:80 --network ssm-network -v $volume_path:/var/local/ssm \
--name ssm alexzhangs/shadowsocks-manager "${default_options[@]}" "$@"
echo "Running $ssm_container_name ..."
docker run --restart=always -d -p 80:80 --network "$ssm_network_name" -v "$ssm_volume_path:/var/local/ssm" \
--name "$ssm_container_name" "$ssm_image" "${default_options[@]}" "$@"
}

main "$@"
Expand Down
43 changes: 43 additions & 0 deletions shadowsocks_manager/notification/fixtures/template-txt-to-json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Description:
This script converts the contents of a template.txt file into a JSON string and prints it.
The template.txt file is read and its contents are stored in the 'content' variable.
Newline characters in the content are replaced with '\r\n'.
A dictionary is created to represent the JSON data, with the template details and other fields.
The dictionary is converted to a JSON string using the json.dumps() function.
The JSON string is printed to the console.
Each carriage return and the position of the template tags in the template.txt file matter.
Usage:
python template-txt-to-json.py > template.json
"""

import json

# Read the contents of template.py
with open('template.txt', 'r') as file:
content = file.read()

# Replace newline characters with \n
content = content.replace('\n', '\r\n')

# Create a dictionary for the JSON data
data = {
"model": "notification.template",
"pk": 1,
"fields": {
"type": "account_created",
"content": content,
"is_active": True,
"dt_created": "2019-05-12T15:37:42.356Z",
"dt_updated": "2019-06-18T20:21:16.078Z"
}
}

# Convert the dictionary to a JSON string
json_data = json.dumps(data, indent=4)

# Print the JSON string
print("\n".join(["[", json_data, "]"]))
2 changes: 1 addition & 1 deletion shadowsocks_manager/notification/fixtures/template.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"pk": 1,
"fields": {
"type": "account_created",
"content": "Subject: CREDENTIAL: VPN Account (Shadowsocks)\r\nTo: {{ account.email }}\r\nDear {{ account.first_name }},\r\n\r\nYour VPN account is setup as below.\r\n\r\n* Port: {{ account.username }}\r\n* Password: {{ account.password }}\r\n\r\nIt's available on {{ node_accounts|length }} node(s):\r\n\r\n{% spaceless %}\r\n{% for obj in node_accounts %}\r\nNode #{{ forloop.counter }}:\r\n------\r\n{% spaceless %}\r\n{% ifchanged %}\r\n* VPN Server: {% if obj.node.domain %} {{ obj.node.domain }} {% else %} {{ obj.node.public_ip }} {% endif %}\r\n* Location: {{ obj.node.location }}\r\n* Encrypt: {{ obj.node.ssmanager.encrypt }}\r\n{% else %}\r\nThis node shares the same settings with above.\r\n{%endifchanged%}\r\n{% endspaceless %}\r\n{% endfor %}\r\n{% endspaceless %}\r\n{% if sender.get_full_name %} {{ sender.get_full_name }} {% else %} {{ '' }} {% endif %}",
"content": "Subject: CREDENTIAL: VPN Account (Shadowsocks)\r\nTo: {{ account.email }}\r\nDear {{ account.first_name }},\r\n\r\nYour VPN account is setup as below.\r\n\r\n* Port: {{ account.username }}\r\n* Password: {{ account.password }}\r\n\r\nIt's available on {{ node_accounts|length }} node(s):\r\n{% for obj in node_accounts %}\r\nNode #{{ forloop.counter }}:\r\n------\r\n{% ifchanged %}\r\n* VPN Server: {% if obj.node.record.fqdn %}{{ obj.node.record.fqdn }}{% else %}{{ obj.node.public_ip }}{% endif %}\r\n* Location: {{ obj.node.location }}\r\n* Encrypt: {{ obj.node.ssmanager.encrypt }}{% if obj.node.ssmanager.is_v2ray_enabled %}\r\n* V2Ray: Required\r\n * Plugin: v2ray-plugin\r\n * Plugin Options: tls;host={{ obj.node.record.fqdn }}\r\n * Mode: Websocket (HTTPS){% endif %}\r\n{% else %}\r\nThis node shares the same settings with above.{%endifchanged%}{% endfor %}\r\n{% if sender.get_full_name %}\r\n{{ sender.get_full_name }}{% endif %}",
"is_active": true,
"dt_created": "2019-05-12T15:37:42.356Z",
"dt_updated": "2019-06-18T20:21:16.078Z"
Expand Down
25 changes: 25 additions & 0 deletions shadowsocks_manager/notification/fixtures/template.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Subject: CREDENTIAL: VPN Account (Shadowsocks)
To: {{ account.email }}
Dear {{ account.first_name }},

Your VPN account is setup as below.

* Port: {{ account.username }}
* Password: {{ account.password }}

It's available on {{ node_accounts|length }} node(s):
{% for obj in node_accounts %}
Node #{{ forloop.counter }}:
------
{% ifchanged %}
* VPN Server: {% if obj.node.record.fqdn %}{{ obj.node.record.fqdn }}{% else %}{{ obj.node.public_ip }}{% endif %}
* Location: {{ obj.node.location }}
* Encrypt: {{ obj.node.ssmanager.encrypt }}{% if obj.node.ssmanager.is_v2ray_enabled %}
* V2Ray: Required
* Plugin: v2ray-plugin
* Plugin Options: tls;host={{ obj.node.record.fqdn }}
* Mode: Websocket (HTTPS){% endif %}
{% else %}
This node shares the same settings with above.{%endifchanged%}{% endfor %}
{% if sender.get_full_name %}
{{ sender.get_full_name }}{% endif %}
2 changes: 1 addition & 1 deletion shadowsocks_manager/shadowsocks/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class SSManagerInline(admin.TabularInline):

fields = ('interface', 'port', 'encrypt', 'is_accessible',
'server_edition', 'is_v2ray_enabled',)
readonly_fields = ('is_accessible', 'is_v2ray_enabled', 'dt_created', 'dt_updated')
readonly_fields = ('is_accessible', 'dt_created', 'dt_updated')

def is_accessible(self, obj):
return obj.is_accessible
Expand Down
4 changes: 2 additions & 2 deletions shadowsocks_manager/shadowsocks/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def save(self, *args, **kwargs):

return ret

def notify(self, sender=None):
def notify(self, sender=User()):
"""
Send account owner the account information by email.
"""
Expand Down Expand Up @@ -536,7 +536,7 @@ class ServerEditionList(enum.Enum):

class SSManager(models.Model):
node = models.ForeignKey(Node, on_delete=models.CASCADE, related_name='ssmanagers')
interface = enum.EnumField(InterfaceList, default=InterfaceList.LOCALHOST,
interface = enum.EnumField(InterfaceList, default=InterfaceList.PRIVATE,
help_text='Network interface bound to Manager API on the node, use an internal '
'interface if possible.')
port = models.PositiveIntegerField(default=6001,
Expand Down

0 comments on commit a5d78c0

Please sign in to comment.