import os
import json
import platform
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils import common_utils
from ansible.module_utils.common_info import DeployStatus
class OpenCodeInstaller:
def __init__(self):
self.module = AnsibleModule(
argument_spec=dict(
tags=dict(type="list", required=True),
resources_dir=dict(type="str", required=True),
)
)
self.run_tags = self.module.params["tags"]
self.resources_dir = os.path.expanduser(self.module.params["resources_dir"])
self.arch = platform.machine()
self.config = None
self.messages = []
self.data_path = ""
def load_opencode_image(self):
opencode_pattern = "opencode*{}.tar*".format(self.arch)
opencode_path = self._find_opencode_pkg(opencode_pattern)
if not self.module.get_bin_path("docker"):
self.module.fail_json("Docker not installed, please install docker first.")
rc, out, err = self.module.run_command(["docker", "load", "-i", opencode_path])
if rc != 0:
self.module.fail_json(
'Failed to loading Docker image from {}, error:{}'.format(opencode_path, err))
self.messages.append("Loading Docker image from {} successfully.".format(opencode_path))
for line in out.splitlines():
if 'Loaded image:' in line:
return line.split()[-1]
return self.module.fail_json('opencode image loaded, but can not get image name.')
def generate_model_config(self, instance_path):
vllm_url = self.config.get("vllm_url", "http://127.0.0.1:8000/v1")
model_name = self.config.get("model_name", "")
config_path = os.path.join(instance_path, ".config/opencode")
self.module.run_command(["mkdir", "-p", config_path], check_rc=True)
model_config = {
"$schema": "https://opencode.ai/config.json",
"provider": {
"vllm": {
"name": "vllm",
"models": {
model_name: {
"name": model_name
}
},
"options": {
"baseURL": vllm_url
}
}
}
}
with open(os.path.join(config_path, "opencode.jsonc"), "w") as f:
json.dump(model_config, f, indent=2)
def generate_auth(self, instance_path):
api_key = self.config.get("api_key", "")
auth_path = os.path.join(instance_path, ".local/share/opencode")
self.module.run_command(["mkdir", "-p", auth_path], check_rc=True)
auth_config = {
"vllm": {
"type": "api",
"key": api_key
}
}
with open(os.path.join(auth_path, "auth.json"), "w") as f:
json.dump(auth_config, f, indent=2)
def generate_docker_compose(self, image_name):
instance_count = self.config.get("instance_count", 1)
base_port = self.config.get("base_port", 8787)
content = ["services:"]
for i in range(instance_count):
instance_path = os.path.join(self.data_path, "instance_{}".format(i))
self.module.run_command(["mkdir", "-p", instance_path], check_rc=True)
self.generate_model_config(instance_path)
self.generate_auth(instance_path)
content.extend([
' opencode-{}:'.format(i),
' image: {}'.format(image_name),
' container_name: opencode-{}'.format(i),
' ports:',
' - "{}:8787"'.format(base_port + i),
' volumes:',
' - {}:/home/opencode'.format(instance_path),
' environment:',
' - HOME=/home/opencode',
' - NODE_TLS_REJECT_UNAUTHORIZED=0',
' command: >',
' serve --hostname 0.0.0.0 --port 8787',
' restart: unless-stopped'
])
with open(os.path.join(self.data_path, "docker-compose.yml"), 'w') as f:
f.write("\n".join(content))
def setup_opencode(self):
if self.module.get_bin_path("docker-compose", required=False):
self.module.run_command(["docker-compose", "up", "-d"],
check_rc=True, cwd=self.data_path)
else:
rc, _, _ = self.module.run_command(["docker", "compose", "version"])
if rc == 0:
self.module.run_command(["docker", "compose", "up", "-d"],
check_rc=True, cwd=self.data_path)
else:
self.module.fail_json(
"Docker Compose not installed, please install docker-compose or docker compose plugin.")
def run(self):
try:
self._get_config()
image_name = self.load_opencode_image()
self.generate_docker_compose(image_name)
self.setup_opencode()
except Exception as e:
self.messages.append(str(e))
self._module_failed()
self._module_success()
def _module_failed(self):
return self.module.fail_json(msg="\n".join(self.messages), rc=1, changed=False)
def _module_success(self):
return self.module.exit_json(msg="Install opencode success.", rc=0, changed=True)
def _find_opencode_pkg(self, pattern):
pkgs, msgs = common_utils.find_files(os.path.join(self.resources_dir, "OpenCode*"), pattern)
self.messages.extend(msgs)
if not pkgs:
if "auto" in self.run_tags:
self.module.exit_json(std_out="can not find opencode package, opencode install skipped", rc=0,
result={DeployStatus.DEPLOY_STATUS: DeployStatus.SKIP},
changed=False)
else:
self.messages.append("can not find opencode package.")
self._module_failed()
return pkgs[0]
def _get_config(self):
config_path = self._find_opencode_pkg("opencode_config.json")
with open(config_path, 'r') as f:
self.config = json.load(f)
self.data_path = self.config.get("data_path", "/data/opencode")
def main():
OpenCodeInstaller().run()
if __name__ == "__main__":
main()